// morphology-es.el - Spanish morphology for the NLG engine. // // Implements fusional Spanish verb conjugation, noun pluralization, gender // inference, and article agreement. Designed as a companion to morphology.el // and called by the engine when the language profile code is "es". // // Verb tenses covered: present, preterite (past), future, imperfect. // Persons: first/second/third × singular/plural (1s 2s 3s 1p 2p 3p). // Verb classes: -ar, -er, -ir (regular) + a core set of common irregulars. // // Depends on: morphology.el (str_ends, str_drop_last, str_last_char, str_last2, str_last3, is_vowel) // ── String helpers (local, matching morphology.el conventions) ──────────────── fn es_str_ends(s: String, suf: String) -> Bool { return str_ends_with(s, suf) } fn es_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 es_str_last_char(s: String) -> String { let n: Int = str_len(s) if n == 0 { return "" } return str_slice(s, n - 1, n) } fn es_str_last2(s: String) -> String { let n: Int = str_len(s) if n < 2 { return s } return str_slice(s, n - 2, n) } fn es_str_last3(s: String) -> String { let n: Int = str_len(s) if n < 3 { return s } return str_slice(s, n - 3, n) } // ── Verb class detection ────────────────────────────────────────────────────── // // Spanish verbs fall into three conjugation classes defined by the infinitive // ending: -ar, -er, -ir. The stem is the infinitive minus those two characters. fn es_verb_class(base: String) -> String { if es_str_ends(base, "ar") { return "ar" } if es_str_ends(base, "er") { return "er" } if es_str_ends(base, "ir") { return "ir" } return "ar" } fn es_stem(base: String) -> String { return es_str_drop_last(base, 2) } // ── Person/number index ─────────────────────────────────────────────────────── // // Maps person × number to a 0-based slot index used inside paradigm tables. // 0 = 1s, 1 = 2s, 2 = 3s, 3 = 1p, 4 = 2p, 5 = 3p fn es_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 if str_eq(number, "singular") { return 2 } return 5 } // ── Irregular present tense ─────────────────────────────────────────────────── // // Returns the fully-inflected form if the verb is irregular in the present // tense for the given person/number slot, otherwise returns "". // // ser: soy, eres, es, somos, sois, son // estar: estoy, estás, está, estamos, estáis, están // tener: tengo, tienes, tiene, tenemos, tenéis, tienen // hacer: hago, haces, hace, hacemos, hacéis, hacen // ir: voy, vas, va, vamos, vais, van // ver: veo, ves, ve, vemos, veis, ven // dar: doy, das, da, damos, dais, dan // saber: sé, sabes, sabe, sabemos, sabéis, saben // poder: puedo, puedes, puede, podemos, podéis, pueden // querer: quiero, quieres, quiere, queremos, queréis, quieren // venir: vengo, vienes, viene, venimos, venís, vienen // decir: digo, dices, dice, decimos, decís, dicen // haber: he, has, ha, hemos, habéis, han fn es_irregular_present(verb: String, person: String, number: String) -> String { let slot: Int = es_slot(person, number) if str_eq(verb, "ser") { if slot == 0 { return "soy" } if slot == 1 { return "eres" } if slot == 2 { return "es" } if slot == 3 { return "somos" } if slot == 4 { return "sois" } return "son" } if str_eq(verb, "estar") { if slot == 0 { return "estoy" } if slot == 1 { return "estás" } if slot == 2 { return "está" } if slot == 3 { return "estamos" } if slot == 4 { return "estáis" } return "están" } if str_eq(verb, "tener") { if slot == 0 { return "tengo" } if slot == 1 { return "tienes" } if slot == 2 { return "tiene" } if slot == 3 { return "tenemos" } if slot == 4 { return "tenéis" } return "tienen" } if str_eq(verb, "hacer") { if slot == 0 { return "hago" } if slot == 1 { return "haces" } if slot == 2 { return "hace" } if slot == 3 { return "hacemos" } if slot == 4 { return "hacéis" } return "hacen" } if str_eq(verb, "ir") { if slot == 0 { return "voy" } if slot == 1 { return "vas" } if slot == 2 { return "va" } if slot == 3 { return "vamos" } if slot == 4 { return "vais" } return "van" } if str_eq(verb, "ver") { if slot == 0 { return "veo" } if slot == 1 { return "ves" } if slot == 2 { return "ve" } if slot == 3 { return "vemos" } if slot == 4 { return "veis" } return "ven" } if str_eq(verb, "dar") { if slot == 0 { return "doy" } if slot == 1 { return "das" } if slot == 2 { return "da" } if slot == 3 { return "damos" } if slot == 4 { return "dais" } return "dan" } if str_eq(verb, "saber") { if slot == 0 { return "sé" } if slot == 1 { return "sabes" } if slot == 2 { return "sabe" } if slot == 3 { return "sabemos" } if slot == 4 { return "sabéis" } return "saben" } if str_eq(verb, "poder") { if slot == 0 { return "puedo" } if slot == 1 { return "puedes" } if slot == 2 { return "puede" } if slot == 3 { return "podemos" } if slot == 4 { return "podéis" } return "pueden" } if str_eq(verb, "querer") { if slot == 0 { return "quiero" } if slot == 1 { return "quieres" } if slot == 2 { return "quiere" } if slot == 3 { return "queremos" } if slot == 4 { return "queréis" } return "quieren" } if str_eq(verb, "venir") { if slot == 0 { return "vengo" } if slot == 1 { return "vienes" } if slot == 2 { return "viene" } if slot == 3 { return "venimos" } if slot == 4 { return "venís" } return "vienen" } if str_eq(verb, "decir") { if slot == 0 { return "digo" } if slot == 1 { return "dices" } if slot == 2 { return "dice" } if slot == 3 { return "decimos" } if slot == 4 { return "decís" } return "dicen" } if str_eq(verb, "haber") { if slot == 0 { return "he" } if slot == 1 { return "has" } if slot == 2 { return "ha" } if slot == 3 { return "hemos" } if slot == 4 { return "habéis" } return "han" } return "" } // ── Irregular preterite tense ───────────────────────────────────────────────── // // Returns the inflected preterite form for irregular verbs, or "" if regular. // // ser/ir (same preterite): fui, fuiste, fue, fuimos, fuisteis, fueron // tener: tuve, tuviste, tuvo, tuvimos, tuvisteis, tuvieron // hacer: hice, hiciste, hizo, hicimos, hicisteis, hicieron // estar: estuve, estuviste, estuvo, estuvimos, estuvisteis, estuvieron // dar: di, diste, dio, dimos, disteis, dieron // saber: supe, supiste, supo, supimos, supisteis, supieron // poder: pude, pudiste, pudo, pudimos, pudisteis, pudieron // querer: quise, quisiste, quiso, quisimos, quisisteis, quisieron // venir: vine, viniste, vino, vinimos, vinisteis, vinieron // decir: dije, dijiste, dijo, dijimos, dijisteis, dijeron // haber: hube, hubiste, hubo, hubimos, hubisteis, hubieron // ver: vi, viste, vio, vimos, visteis, vieron fn es_irregular_preterite(verb: String, person: String, number: String) -> String { let slot: Int = es_slot(person, number) if str_eq(verb, "ser") { if slot == 0 { return "fui" } if slot == 1 { return "fuiste" } if slot == 2 { return "fue" } if slot == 3 { return "fuimos" } if slot == 4 { return "fuisteis" } return "fueron" } if str_eq(verb, "ir") { if slot == 0 { return "fui" } if slot == 1 { return "fuiste" } if slot == 2 { return "fue" } if slot == 3 { return "fuimos" } if slot == 4 { return "fuisteis" } return "fueron" } if str_eq(verb, "tener") { if slot == 0 { return "tuve" } if slot == 1 { return "tuviste" } if slot == 2 { return "tuvo" } if slot == 3 { return "tuvimos" } if slot == 4 { return "tuvisteis" } return "tuvieron" } if str_eq(verb, "hacer") { if slot == 0 { return "hice" } if slot == 1 { return "hiciste" } if slot == 2 { return "hizo" } if slot == 3 { return "hicimos" } if slot == 4 { return "hicisteis" } return "hicieron" } if str_eq(verb, "estar") { if slot == 0 { return "estuve" } if slot == 1 { return "estuviste" } if slot == 2 { return "estuvo" } if slot == 3 { return "estuvimos" } if slot == 4 { return "estuvisteis" } return "estuvieron" } if str_eq(verb, "dar") { if slot == 0 { return "di" } if slot == 1 { return "diste" } if slot == 2 { return "dio" } if slot == 3 { return "dimos" } if slot == 4 { return "disteis" } return "dieron" } if str_eq(verb, "saber") { if slot == 0 { return "supe" } if slot == 1 { return "supiste" } if slot == 2 { return "supo" } if slot == 3 { return "supimos" } if slot == 4 { return "supisteis" } return "supieron" } if str_eq(verb, "poder") { if slot == 0 { return "pude" } if slot == 1 { return "pudiste" } if slot == 2 { return "pudo" } if slot == 3 { return "pudimos" } if slot == 4 { return "pudisteis" } return "pudieron" } if str_eq(verb, "querer") { if slot == 0 { return "quise" } if slot == 1 { return "quisiste" } if slot == 2 { return "quiso" } if slot == 3 { return "quisimos" } if slot == 4 { return "quisisteis" } return "quisieron" } if str_eq(verb, "venir") { if slot == 0 { return "vine" } if slot == 1 { return "viniste" } if slot == 2 { return "vino" } if slot == 3 { return "vinimos" } if slot == 4 { return "vinisteis" } return "vinieron" } if str_eq(verb, "decir") { if slot == 0 { return "dije" } if slot == 1 { return "dijiste" } if slot == 2 { return "dijo" } if slot == 3 { return "dijimos" } if slot == 4 { return "dijisteis" } return "dijeron" } if str_eq(verb, "haber") { if slot == 0 { return "hube" } if slot == 1 { return "hubiste" } if slot == 2 { return "hubo" } if slot == 3 { return "hubimos" } if slot == 4 { return "hubisteis" } return "hubieron" } if str_eq(verb, "ver") { if slot == 0 { return "vi" } if slot == 1 { return "viste" } if slot == 2 { return "vio" } if slot == 3 { return "vimos" } if slot == 4 { return "visteis" } return "vieron" } return "" } // ── Irregular imperfect tense ───────────────────────────────────────────────── // // Only three verbs are truly irregular in the imperfect: // ser: era, eras, era, éramos, erais, eran // ir: iba, ibas, iba, íbamos, ibais, iban // ver: veía, veías, veía, veíamos, veíais, veían fn es_irregular_imperfect(verb: String, person: String, number: String) -> String { let slot: Int = es_slot(person, number) if str_eq(verb, "ser") { if slot == 0 { return "era" } if slot == 1 { return "eras" } if slot == 2 { return "era" } if slot == 3 { return "éramos" } if slot == 4 { return "erais" } return "eran" } if str_eq(verb, "ir") { if slot == 0 { return "iba" } if slot == 1 { return "ibas" } if slot == 2 { return "iba" } if slot == 3 { return "íbamos" } if slot == 4 { return "ibais" } return "iban" } if str_eq(verb, "ver") { if slot == 0 { return "veía" } if slot == 1 { return "veías" } if slot == 2 { return "veía" } if slot == 3 { return "veíamos" } if slot == 4 { return "veíais" } return "veían" } return "" } // ── Regular present conjugation ─────────────────────────────────────────────── // // -ar: -o, -as, -a, -amos, -áis, -an // -er: -o, -es, -e, -emos, -éis, -en // -ir: -o, -es, -e, -imos, -ís, -en fn es_regular_present(stem: String, vclass: String, slot: Int) -> String { if str_eq(vclass, "ar") { if slot == 0 { return stem + "o" } if slot == 1 { return stem + "as" } if slot == 2 { return stem + "a" } if slot == 3 { return stem + "amos" } if slot == 4 { return stem + "áis" } return stem + "an" } if str_eq(vclass, "er") { if slot == 0 { return stem + "o" } if slot == 1 { return stem + "es" } if slot == 2 { return stem + "e" } if slot == 3 { return stem + "emos" } if slot == 4 { return stem + "éis" } return stem + "en" } // -ir if slot == 0 { return stem + "o" } if slot == 1 { return stem + "es" } if slot == 2 { return stem + "e" } if slot == 3 { return stem + "imos" } if slot == 4 { return stem + "ís" } return stem + "en" } // ── Regular preterite conjugation ───────────────────────────────────────────── // // -ar: -é, -aste, -ó, -amos, -asteis, -aron // -er: -í, -iste, -ió, -imos, -isteis, -ieron // -ir: -í, -iste, -ió, -imos, -isteis, -ieron fn es_regular_preterite(stem: String, vclass: String, slot: Int) -> String { if str_eq(vclass, "ar") { if slot == 0 { return stem + "é" } if slot == 1 { return stem + "aste" } if slot == 2 { return stem + "ó" } if slot == 3 { return stem + "amos" } if slot == 4 { return stem + "asteis" } return stem + "aron" } // -er and -ir share the same preterite endings if slot == 0 { return stem + "í" } if slot == 1 { return stem + "iste" } if slot == 2 { return stem + "ió" } if slot == 3 { return stem + "imos" } if slot == 4 { return stem + "isteis" } return stem + "ieron" } // ── Regular future conjugation ──────────────────────────────────────────────── // // Future is formed from the full infinitive + endings (all classes): // -é, -ás, -á, -emos, -éis, -án // // No stem change; the infinitive is the future stem. fn es_regular_future(base: String, slot: Int) -> String { if slot == 0 { return base + "é" } if slot == 1 { return base + "ás" } if slot == 2 { return base + "á" } if slot == 3 { return base + "emos" } if slot == 4 { return base + "éis" } return base + "án" } // ── Irregular future stems ──────────────────────────────────────────────────── // // Some verbs contract or alter their infinitive for the future stem. // Returns the irregular future stem, or "" if the verb uses the regular stem. fn es_irregular_future_stem(verb: String) -> String { if str_eq(verb, "tener") { return "tendr" } if str_eq(verb, "hacer") { return "har" } if str_eq(verb, "poder") { return "podr" } if str_eq(verb, "querer") { return "querr" } if str_eq(verb, "venir") { return "vendr" } if str_eq(verb, "decir") { return "dir" } if str_eq(verb, "haber") { return "habr" } if str_eq(verb, "saber") { return "sabr" } if str_eq(verb, "salir") { return "saldr" } if str_eq(verb, "poner") { return "pondr" } return "" } // ── Regular imperfect conjugation ───────────────────────────────────────────── // // -ar: -aba, -abas, -aba, -ábamos, -abais, -aban // -er/-ir: -ía, -ías, -ía, -íamos, -íais, -ían fn es_regular_imperfect(stem: String, vclass: String, slot: Int) -> String { if str_eq(vclass, "ar") { if slot == 0 { return stem + "aba" } if slot == 1 { return stem + "abas" } if slot == 2 { return stem + "aba" } if slot == 3 { return stem + "ábamos" } if slot == 4 { return stem + "abais" } return stem + "aban" } // -er and -ir if slot == 0 { return stem + "ía" } if slot == 1 { return stem + "ías" } if slot == 2 { return stem + "ía" } if slot == 3 { return stem + "íamos" } if slot == 4 { return stem + "íais" } return stem + "ían" } // ── Full conjugation entry point ────────────────────────────────────────────── // // es_conjugate: conjugate a Spanish verb. // // verb: Spanish infinitive (e.g. "hablar", "ser", "tener") // tense: "present" | "past" | "future" | "imperfect" // (note: "past" maps to the preterite/indefinite past) // person: "first" | "second" | "third" // number: "singular" | "plural" fn es_conjugate(verb: String, tense: String, person: String, number: String) -> String { let slot: Int = es_slot(person, number) if str_eq(tense, "present") { let irreg: String = es_irregular_present(verb, person, number) if !str_eq(irreg, "") { return irreg } let vclass: String = es_verb_class(verb) let stem: String = es_stem(verb) return es_regular_present(stem, vclass, slot) } if str_eq(tense, "past") { let irreg: String = es_irregular_preterite(verb, person, number) if !str_eq(irreg, "") { return irreg } let vclass: String = es_verb_class(verb) let stem: String = es_stem(verb) return es_regular_preterite(stem, vclass, slot) } if str_eq(tense, "future") { let irreg_stem: String = es_irregular_future_stem(verb) if !str_eq(irreg_stem, "") { return es_regular_future(irreg_stem, slot) } return es_regular_future(verb, slot) } if str_eq(tense, "imperfect") { let irreg: String = es_irregular_imperfect(verb, person, number) if !str_eq(irreg, "") { return irreg } let vclass: String = es_verb_class(verb) let stem: String = es_stem(verb) return es_regular_imperfect(stem, vclass, slot) } // Unknown tense: return infinitive unchanged return verb } // ── Noun gender inference ───────────────────────────────────────────────────── // // Returns "m" (masculine), "f" (feminine), or "unknown". // // Heuristics (not exhaustive — cover most common patterns): // ends in -o -> masculine (libro, gato, niño) // ends in -a -> feminine (casa, mesa, niña) // ends in -ión -> feminine (canción, nación) // ends in -dad/-tad -> feminine (ciudad, libertad) // ends in -umbre -> feminine (costumbre) // ends in -sis -> feminine (crisis, tesis) // ends in -ema/-ama -> masculine (problema, programa, tema, idioma) // ends in -or -> masculine (color, amor, señor) // ends in -aje -> masculine (viaje, paisaje) // ends in -án/-ón -> masculine (avión → check -ión first) // otherwise -> unknown fn es_gender(noun: String) -> String { // -ión before -o so "avión" → feminine (it ends -ión, not just -on) if es_str_ends(noun, "ión") { return "f" } if es_str_ends(noun, "dad") { return "f" } if es_str_ends(noun, "tad") { return "f" } if es_str_ends(noun, "umbre") { return "f" } if es_str_ends(noun, "sis") { return "f" } if es_str_ends(noun, "ema") { return "m" } if es_str_ends(noun, "ama") { return "m" } if es_str_ends(noun, "aje") { return "m" } if es_str_ends(noun, "or") { return "m" } if es_str_ends(noun, "o") { return "m" } if es_str_ends(noun, "a") { return "f" } return "unknown" } // ── Noun pluralization ──────────────────────────────────────────────────────── // // Rules (applied in order): // ends in vowel (a e i o u) -> add -s // ends in consonant -> add -es // ends in -z -> replace -z with -ces // ends in -s (unstressed) -> unchanged (e.g. "el lunes" -> "los lunes") // // Note: nouns ending in stressed vowel + s (e.g. "el autobús" → "los autobuses") // are handled by the consonant rule since -s is a consonant ending for pluralization // purposes; but "el lunes" (days of week ending in -s) stay unchanged — this is // an irregular class. The table below handles common invariant nouns. fn es_invariant_plural(noun: String) -> String { if str_eq(noun, "lunes") { return "lunes" } if str_eq(noun, "martes") { return "martes" } if str_eq(noun, "miércoles") { return "miércoles" } if str_eq(noun, "jueves") { return "jueves" } if str_eq(noun, "viernes") { return "viernes" } if str_eq(noun, "crisis") { return "crisis" } if str_eq(noun, "tesis") { return "tesis" } if str_eq(noun, "análisis") { return "análisis" } if str_eq(noun, "dosis") { return "dosis" } if str_eq(noun, "virus") { return "virus" } return "" } fn es_pluralize(noun: String) -> String { let inv: String = es_invariant_plural(noun) if !str_eq(inv, "") { return inv } let last: String = es_str_last_char(noun) // Ends in -z: replace with -ces if str_eq(last, "z") { return es_str_drop_last(noun, 1) + "ces" } // Ends in a vowel: add -s if str_eq(last, "a") { return noun + "s" } if str_eq(last, "e") { return noun + "s" } if str_eq(last, "i") { return noun + "s" } if str_eq(last, "o") { return noun + "s" } if str_eq(last, "u") { return noun + "s" } // Ends in consonant (including -s for stressed words like autobús): add -es return noun + "es" } // ── Article agreement ───────────────────────────────────────────────────────── // // es_agree_article: return the correct Spanish article for a noun. // // noun: the noun (used for gender and number inference) // definite: "true" for definite (el/la/los/las), "false" for indefinite (un/una/unos/unas) // number: "singular" | "plural" // // Special case: feminine nouns beginning with stressed "a-" or "ha-" take // masculine singular definite article: "el agua", "el hacha". // This is handled by checking the noun's first character when gender is feminine. fn es_starts_with_stressed_a(noun: String) -> Bool { // Approximate: check if noun starts with "a" or "ha" (covers most cases) // The accent on the first syllable is not detectable orthographically in // general, so we apply the rule broadly for any feminine noun starting with // "a" or "ha" in singular. let n: Int = str_len(noun) if n == 0 { return false } let c0: String = str_slice(noun, 0, 1) if str_eq(c0, "a") { return true } if n >= 2 { let c1: String = str_slice(noun, 1, 2) if str_eq(c0, "h") { if str_eq(c1, "a") { return true } } } return false } fn es_agree_article(noun: String, definite: String, number: String) -> String { let gender: String = es_gender(noun) let is_plural: Bool = str_eq(number, "plural") let is_def: Bool = str_eq(definite, "true") if is_def { if is_plural { if str_eq(gender, "f") { return "las" } return "los" } // singular if str_eq(gender, "f") { // el agua rule: feminine singular nouns starting with stressed "a" if es_starts_with_stressed_a(noun) { return "el" } return "la" } return "el" } // indefinite if is_plural { if str_eq(gender, "f") { return "unas" } return "unos" } if str_eq(gender, "f") { return "una" } return "un" }