Files
el/elp/src/morphology-fr.el
T
will.anderson c2cd5e01e1
El SDK CI - dev / build-and-test (pull_request) Successful in 3m34s
fix: elb macOS OpenSSL + C master declarations header; add ELP missing imports
elb.el:
- Auto-detect Homebrew OpenSSL (-L$(brew --prefix openssl)/lib) so -lssl
  resolves on macOS without manual flags; no-op on Linux
- Add -include elp-c-decls.h when present in out_dir: resolves undeclared
  cross-module calls in packages like ELP that lack explicit imports

ELP source:
- Add import "morphology.el" to all 29 language morphology modules
- Add language module imports to morphology.el (all langs it dispatches to)
  These were missing since ELP was originally built as a monolithic unit
2026-05-08 19:44:31 -05:00

678 lines
25 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// morphology-fr.el - French morphology for the NLG engine.
//
// Implements fusional French verb conjugation, noun pluralization, gender
// inference, article agreement, and interrogative inversion. Designed as a
// companion to morphology.el and called by the engine when the language
// profile code is "fr".
//
// Verb tenses covered: present, future, imparfait, passé composé.
// Persons: first/second/third × singular/plural.
// Verb groups: -er (regular), -ir (regular finir-type), -re (regular vendre-type)
// + a core set of common irregular verbs.
//
// Liaison / elision notes:
// - le/la l' before vowel-initial nouns (handled in fr_agree_article)
// - est-ce que can precede any statement for yes/no questions (fr_question_inversion)
// - Inversion inserts -t- between vowel-final verb form and 3s pronoun: parle-t-il
//
// Depends on: morphology.el (str_ends_with, str_slice, str_len helpers)
// String helpers (local, matching morphology.el conventions)
import "morphology.el"
fn fr_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn fr_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 fr_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
fn fr_str_last2(s: String) -> String {
let n: Int = str_len(s)
if n < 2 {
return s
}
return str_slice(s, n - 2, n)
}
fn fr_is_vowel_start(s: String) -> Bool {
let n: Int = str_len(s)
if n == 0 {
return false
}
let c: String = str_slice(s, 0, 1)
if str_eq(c, "a") { return true }
if str_eq(c, "e") { return true }
if str_eq(c, "é") { return true }
if str_eq(c, "è") { return true }
if str_eq(c, "ê") { return true }
if str_eq(c, "i") { return true }
if str_eq(c, "î") { return true }
if str_eq(c, "o") { return true }
if str_eq(c, "ô") { return true }
if str_eq(c, "u") { return true }
if str_eq(c, "û") { return true }
if str_eq(c, "h") { return true }
return false
}
// Verb group detection
//
// Returns "er" | "ir" | "re" | "irregular".
// Irregular detection is done by checking against a known list first; all other
// verbs are classified by ending.
fn fr_is_known_irregular(verb: String) -> Bool {
if str_eq(verb, "être") { return true }
if str_eq(verb, "avoir") { return true }
if str_eq(verb, "aller") { return true }
if str_eq(verb, "faire") { return true }
if str_eq(verb, "pouvoir") { return true }
if str_eq(verb, "vouloir") { return true }
if str_eq(verb, "venir") { return true }
if str_eq(verb, "dire") { return true }
if str_eq(verb, "voir") { return true }
if str_eq(verb, "prendre") { return true }
if str_eq(verb, "mettre") { return true }
if str_eq(verb, "savoir") { return true }
return false
}
fn fr_verb_group(base: String) -> String {
if fr_is_known_irregular(base) { return "irregular" }
if fr_str_ends(base, "er") { return "er" }
if fr_str_ends(base, "ir") { return "ir" }
if fr_str_ends(base, "re") { return "re" }
return "er"
}
fn fr_stem(base: String) -> String {
return fr_str_drop_last(base, 2)
}
// Person/number slot index
//
// 0 = 1s (je), 1 = 2s (tu), 2 = 3s (il/elle), 3 = 1p (nous), 4 = 2p (vous), 5 = 3p (ils/elles)
fn fr_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
}
if str_eq(number, "singular") { return 2 }
return 5
}
// Irregular present tense
//
// être: suis, es, est, sommes, êtes, sont
// avoir: ai, as, a, avons, avez, ont
// aller: vais, vas, va, allons, allez, vont
// faire: fais, fais, fait, faisons, faites, font
// pouvoir: peux, peux, peut, pouvons, pouvez, peuvent
// vouloir: veux, veux, veut, voulons, voulez, veulent
// venir: viens, viens, vient, venons, venez, viennent
// dire: dis, dis, dit, disons, dites, disent
// voir: vois, vois, voit, voyons, voyez, voient
// prendre: prends, prends, prend, prenons, prenez, prennent
// mettre: mets, mets, met, mettons, mettez, mettent
// savoir: sais, sais, sait, savons, savez, savent
fn fr_irregular_present(verb: String, person: String, number: String) -> String {
let slot: Int = fr_slot(person, number)
if str_eq(verb, "être") {
if slot == 0 { return "suis" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sommes" }
if slot == 4 { return "etes" }
return "sont"
}
// ASCII alias used by morph_map_canonical identical conjugation.
if str_eq(verb, "etre") {
if slot == 0 { return "suis" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sommes" }
if slot == 4 { return "etes" }
return "sont"
}
if str_eq(verb, "avoir") {
if slot == 0 { return "ai" }
if slot == 1 { return "as" }
if slot == 2 { return "a" }
if slot == 3 { return "avons" }
if slot == 4 { return "avez" }
return "ont"
}
if str_eq(verb, "aller") {
if slot == 0 { return "vais" }
if slot == 1 { return "vas" }
if slot == 2 { return "va" }
if slot == 3 { return "allons" }
if slot == 4 { return "allez" }
return "vont"
}
if str_eq(verb, "faire") {
if slot == 0 { return "fais" }
if slot == 1 { return "fais" }
if slot == 2 { return "fait" }
if slot == 3 { return "faisons" }
if slot == 4 { return "faites" }
return "font"
}
if str_eq(verb, "pouvoir") {
if slot == 0 { return "peux" }
if slot == 1 { return "peux" }
if slot == 2 { return "peut" }
if slot == 3 { return "pouvons" }
if slot == 4 { return "pouvez" }
return "peuvent"
}
if str_eq(verb, "vouloir") {
if slot == 0 { return "veux" }
if slot == 1 { return "veux" }
if slot == 2 { return "veut" }
if slot == 3 { return "voulons" }
if slot == 4 { return "voulez" }
return "veulent"
}
if str_eq(verb, "venir") {
if slot == 0 { return "viens" }
if slot == 1 { return "viens" }
if slot == 2 { return "vient" }
if slot == 3 { return "venons" }
if slot == 4 { return "venez" }
return "viennent"
}
if str_eq(verb, "dire") {
if slot == 0 { return "dis" }
if slot == 1 { return "dis" }
if slot == 2 { return "dit" }
if slot == 3 { return "disons" }
if slot == 4 { return "dites" }
return "disent"
}
if str_eq(verb, "voir") {
if slot == 0 { return "vois" }
if slot == 1 { return "vois" }
if slot == 2 { return "voit" }
if slot == 3 { return "voyons" }
if slot == 4 { return "voyez" }
return "voient"
}
if str_eq(verb, "prendre") {
if slot == 0 { return "prends" }
if slot == 1 { return "prends" }
if slot == 2 { return "prend" }
if slot == 3 { return "prenons" }
if slot == 4 { return "prenez" }
return "prennent"
}
if str_eq(verb, "mettre") {
if slot == 0 { return "mets" }
if slot == 1 { return "mets" }
if slot == 2 { return "met" }
if slot == 3 { return "mettons" }
if slot == 4 { return "mettez" }
return "mettent"
}
if str_eq(verb, "savoir") {
if slot == 0 { return "sais" }
if slot == 1 { return "sais" }
if slot == 2 { return "sait" }
if slot == 3 { return "savons" }
if slot == 4 { return "savez" }
return "savent"
}
return ""
}
// Regular present tense
//
// -er: -e, -es, -e, -ons, -ez, -ent
// -ir: -is, -is, -it, -issons, -issez, -issent (finir-type; stem gets -iss- in plural)
// -re: -s, -s, -(nothing), -ons, -ez, -ent
fn fr_regular_present(stem: String, vgroup: String, slot: Int) -> String {
if str_eq(vgroup, "er") {
if slot == 0 { return stem + "e" }
if slot == 1 { return stem + "es" }
if slot == 2 { return stem + "e" }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
if str_eq(vgroup, "ir") {
// finir-type: singular uses bare stem, plural uses stem + -iss-
if slot == 0 { return stem + "is" }
if slot == 1 { return stem + "is" }
if slot == 2 { return stem + "it" }
if slot == 3 { return stem + "issons" }
if slot == 4 { return stem + "issez" }
return stem + "issent"
}
// -re (vendre-type)
if slot == 0 { return stem + "s" }
if slot == 1 { return stem + "s" }
if slot == 2 { return stem }
if slot == 3 { return stem + "ons" }
if slot == 4 { return stem + "ez" }
return stem + "ent"
}
// Regular future tense
//
// Future is formed from the infinitive (minus silent -e for -re verbs) + endings:
// -ai, -as, -a, -ons, -ez, -ont
fn fr_future_stem(base: String, vgroup: String) -> String {
// -re verbs drop the final -e before adding future endings
if str_eq(vgroup, "re") {
return fr_str_drop_last(base, 1)
}
return base
}
fn fr_regular_future(fstem: String, slot: Int) -> String {
if slot == 0 { return fstem + "ai" }
if slot == 1 { return fstem + "as" }
if slot == 2 { return fstem + "a" }
if slot == 3 { return fstem + "ons" }
if slot == 4 { return fstem + "ez" }
return fstem + "ont"
}
// Irregular future stems
//
// Returns the irregular future stem, or "" if regular.
fn fr_irregular_future_stem(verb: String) -> String {
if str_eq(verb, "être") { return "ser" }
if str_eq(verb, "avoir") { return "aur" }
if str_eq(verb, "aller") { return "ir" }
if str_eq(verb, "faire") { return "fer" }
if str_eq(verb, "pouvoir") { return "pourr" }
if str_eq(verb, "vouloir") { return "voudr" }
if str_eq(verb, "venir") { return "viendr" }
if str_eq(verb, "voir") { return "verr" }
if str_eq(verb, "savoir") { return "saur" }
return ""
}
// Regular imparfait
//
// Imparfait is formed from the nous-present stem (infinitive minus -er/-ir/-re,
// then add -iss for -ir verbs in nous-form) + endings:
// -ais, -ais, -ait, -ions, -iez, -aient
//
// For -er verbs: stem = infinitive minus -er
// For -ir verbs: stem = infinitive minus -ir (bare stem, not -iss- imparfait
// uses the basic stem, unlike present plural which uses -iss-)
// For -re verbs: stem = infinitive minus -re
// Exception: être uses ét- as the imparfait stem.
fn fr_imperfect_stem(base: String, vgroup: String) -> String {
if str_eq(base, "être") { return "ét" }
return fr_stem(base)
}
fn fr_regular_imperfect(istem: String, slot: Int) -> String {
if slot == 0 { return istem + "ais" }
if slot == 1 { return istem + "ais" }
if slot == 2 { return istem + "ait" }
if slot == 3 { return istem + "ions" }
if slot == 4 { return istem + "iez" }
return istem + "aient"
}
// Passé composé (past compound)
//
// Passé composé = auxiliary (avoir or être) + past participle.
// Most verbs use avoir; a core set of motion/state verbs use être.
//
// This function returns a two-word string "auxiliary participle".
// The caller is responsible for agreement of the past participle when être is
// used (feminine adds -e, plural adds -s); this function returns the masculine
// singular participle unconditionally.
fn fr_uses_etre(verb: String) -> Bool {
if str_eq(verb, "aller") { return true }
if str_eq(verb, "venir") { return true }
if str_eq(verb, "partir") { return true }
if str_eq(verb, "arriver") { return true }
if str_eq(verb, "entrer") { return true }
if str_eq(verb, "sortir") { return true }
if str_eq(verb, "naître") { return true }
if str_eq(verb, "mourir") { return true }
if str_eq(verb, "rester") { return true }
if str_eq(verb, "tomber") { return true }
if str_eq(verb, "monter") { return true }
if str_eq(verb, "descendre") { return true }
if str_eq(verb, "rentrer") { return true }
if str_eq(verb, "retourner") { return true }
if str_eq(verb, "passer") { return true }
return false
}
// Returns the past participle (masculine singular form).
fn fr_past_participle(verb: String) -> String {
// Irregular participles
if str_eq(verb, "être") { return "été" }
if str_eq(verb, "avoir") { return "eu" }
if str_eq(verb, "aller") { return "allé" }
if str_eq(verb, "faire") { return "fait" }
if str_eq(verb, "pouvoir") { return "pu" }
if str_eq(verb, "vouloir") { return "voulu" }
if str_eq(verb, "venir") { return "venu" }
if str_eq(verb, "dire") { return "dit" }
if str_eq(verb, "voir") { return "vu" }
if str_eq(verb, "prendre") { return "pris" }
if str_eq(verb, "mettre") { return "mis" }
if str_eq(verb, "savoir") { return "su" }
if str_eq(verb, "naître") { return "" }
if str_eq(verb, "mourir") { return "mort" }
// Regular participles by group
let vgroup: String = fr_verb_group(verb)
if str_eq(vgroup, "er") {
return fr_str_drop_last(verb, 2) + "é"
}
if str_eq(vgroup, "ir") {
return fr_str_drop_last(verb, 2) + "i"
}
// -re verbs: drop -re, add -u
return fr_str_drop_last(verb, 2) + "u"
}
// Conjugates the avoir auxiliary in the present (for passé composé).
fn fr_avoir_present(slot: Int) -> String {
if slot == 0 { return "ai" }
if slot == 1 { return "as" }
if slot == 2 { return "a" }
if slot == 3 { return "avons" }
if slot == 4 { return "avez" }
return "ont"
}
// Conjugates the être auxiliary in the present (for passé composé).
fn fr_etre_present(slot: Int) -> String {
if slot == 0 { return "suis" }
if slot == 1 { return "es" }
if slot == 2 { return "est" }
if slot == 3 { return "sommes" }
if slot == 4 { return "êtes" }
return "sont"
}
// Full conjugation entry point
//
// fr_conjugate: conjugate a French verb.
//
// verb: French infinitive (e.g. "parler", "être", "venir")
// tense: "present" | "future" | "imperfect" | "past"
// (note: "past" returns the passé composé as "aux participle")
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn fr_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let slot: Int = fr_slot(person, number)
if str_eq(tense, "present") {
let irreg: String = fr_irregular_present(verb, person, number)
if !str_eq(irreg, "") {
return irreg
}
let vgroup: String = fr_verb_group(verb)
let stem: String = fr_stem(verb)
return fr_regular_present(stem, vgroup, slot)
}
if str_eq(tense, "future") {
let irreg_stem: String = fr_irregular_future_stem(verb)
if !str_eq(irreg_stem, "") {
return fr_regular_future(irreg_stem, slot)
}
let vgroup: String = fr_verb_group(verb)
let fstem: String = fr_future_stem(verb, vgroup)
return fr_regular_future(fstem, slot)
}
if str_eq(tense, "imperfect") {
let vgroup: String = fr_verb_group(verb)
let istem: String = fr_imperfect_stem(verb, vgroup)
return fr_regular_imperfect(istem, slot)
}
if str_eq(tense, "past") {
// Passé composé: auxiliary + past participle
let pp: String = fr_past_participle(verb)
if fr_uses_etre(verb) {
let aux: String = fr_etre_present(slot)
return aux + " " + pp
}
let aux: String = fr_avoir_present(slot)
return aux + " " + pp
}
// Unknown tense: return infinitive unchanged
return verb
}
// Noun gender inference
//
// Returns "m" (masculine), "f" (feminine), or "unknown".
//
// Heuristics (common French patterns):
// ends in -tion/-sion/-xion -> feminine (nation, passion, connexion)
// ends in -ure -> feminine (voiture, culture)
// ends in -ette -> feminine (omelette, cigarette)
// ends in -eur (abstract) -> feminine (couleur, peur, chaleur)
// ends in -eur (agent) -> masculine (acteur, serveur) can't always distinguish
// ends in -eur (no rule) -> try -teur masculine (docteur, auteur)
// ends in -ment -> masculine (sentiment, gouvernement)
// ends in -age -> masculine (voyage, fromage)
// ends in -isme -> masculine (socialisme)
// ends in -eau -> masculine (tableau, gâteau)
// ends in -er/-é -> masculine (boucher, café)
// ends in -ée -> feminine (journée, idée)
// ends in -ie -> feminine (philosophie, géographie)
// ends in -ance/-ence -> feminine (chance, science)
// ends in -té/-tié -> feminine (beauté, amitié)
// ends in -ude -> feminine (attitude, solitude)
// ends in -ade -> feminine (salade, promenade)
// ends in -ette -> feminine (already covered)
// ends in -e (generic) -> often feminine but not reliable; return "unknown"
fn fr_gender(noun: String) -> String {
// Feminine patterns (check more specific before general)
if fr_str_ends(noun, "tion") { return "f" }
if fr_str_ends(noun, "sion") { return "f" }
if fr_str_ends(noun, "xion") { return "f" }
if fr_str_ends(noun, "ure") { return "f" }
if fr_str_ends(noun, "ette") { return "f" }
if fr_str_ends(noun, "ance") { return "f" }
if fr_str_ends(noun, "ence") { return "f" }
if fr_str_ends(noun, "ité") { return "f" }
if fr_str_ends(noun, "") { return "f" }
if fr_str_ends(noun, "tié") { return "f" }
if fr_str_ends(noun, "ude") { return "f" }
if fr_str_ends(noun, "ade") { return "f" }
if fr_str_ends(noun, "ée") { return "f" }
if fr_str_ends(noun, "ie") { return "f" }
// Masculine patterns
if fr_str_ends(noun, "ment") { return "m" }
if fr_str_ends(noun, "age") { return "m" }
if fr_str_ends(noun, "isme") { return "m" }
if fr_str_ends(noun, "eau") { return "m" }
if fr_str_ends(noun, "eur") { return "m" }
if fr_str_ends(noun, "er") { return "m" }
if fr_str_ends(noun, "é") { return "m" }
return "unknown"
}
// Noun pluralization
//
// French plural rules:
// already ends in -s, -x, or -z -> unchanged
// ends in -eau -> add -x (bateau bateaux)
// ends in -eu -> add -x (jeu jeux; bleu → bleus is exception)
// ends in -al -> replace -al with -aux (animal animaux)
// ends in -ail (most) -> replace -ail with -aux (travail travaux; bail)
// otherwise -> add -s
fn fr_invariant_plural(noun: String) -> String {
// Words already ending in -s, -x, -z are unchanged in plural
let last: String = fr_str_last_char(noun)
if str_eq(last, "s") { return noun }
if str_eq(last, "x") { return noun }
if str_eq(last, "z") { return noun }
return ""
}
fn fr_pluralize(noun: String) -> String {
let inv: String = fr_invariant_plural(noun)
if !str_eq(inv, "") {
return inv
}
if fr_str_ends(noun, "eau") {
return noun + "x"
}
if fr_str_ends(noun, "eu") {
return noun + "x"
}
if fr_str_ends(noun, "al") {
return fr_str_drop_last(noun, 2) + "aux"
}
if fr_str_ends(noun, "ail") {
return fr_str_drop_last(noun, 3) + "aux"
}
return noun + "s"
}
// Article agreement
//
// fr_agree_article: return the correct French article for a noun.
//
// noun: the noun (used for gender inference and elision check)
// definite: "true" for definite (le/la/l'/les), "false" for indefinite (un/une/des)
// number: "singular" | "plural"
//
// Elision: le/la l' before a vowel- or h-initial noun (handled here).
fn fr_agree_article(noun: String, definite: String, number: String) -> String {
let gender: String = fr_gender(noun)
let is_plural: Bool = str_eq(number, "plural")
let is_def: Bool = str_eq(definite, "true")
let vowel_start: Bool = fr_is_vowel_start(noun)
if is_def {
if is_plural {
return "les"
}
// singular
if vowel_start {
return "l'"
}
if str_eq(gender, "f") {
return "la"
}
return "le"
}
// indefinite
if is_plural {
return "des"
}
if str_eq(gender, "f") {
return "une"
}
return "un"
}
// Question inversion
//
// fr_question_inversion: form a yes/no question using subject-verb inversion.
//
// subject: pronoun string: "je" | "tu" | "il" | "elle" | "nous" | "vous" | "ils" | "elles"
// verb_form: the conjugated verb form (e.g. "parle", "mange", "est")
//
// Rules:
// - Verb and subject are joined with a hyphen: "parle-t-il ?"
// - When the verb form ends in a vowel and the subject starts with a vowel
// (il, elle, ils, elles), insert euphonic -t-: "parle-t-il ?"
// - Je inversion is archaic; "est-ce que je ...?" is preferred — this function
// generates "est-ce que je <verb_form> ?" for first-person singular.
// - The result ends with " ?"
fn fr_subject_starts_vowel(subject: String) -> Bool {
if str_eq(subject, "il") { return true }
if str_eq(subject, "elle") { return true }
if str_eq(subject, "ils") { return true }
if str_eq(subject, "elles") { return true }
return false
}
fn fr_verb_ends_vowel(verb_form: String) -> Bool {
let last: String = fr_str_last_char(verb_form)
if str_eq(last, "a") { return true }
if str_eq(last, "e") { return true }
if str_eq(last, "é") { return true }
if str_eq(last, "i") { return true }
if str_eq(last, "o") { return true }
if str_eq(last, "u") { return true }
return false
}
fn fr_question_inversion(subject: String, verb_form: String) -> String {
// First-person singular: use est-ce que construction
if str_eq(subject, "je") {
return "est-ce que je " + verb_form + " ?"
}
// Determine whether to insert -t-
let need_t: Bool = false
if fr_verb_ends_vowel(verb_form) {
if fr_subject_starts_vowel(subject) {
let need_t = true
}
}
if need_t {
return verb_form + "-t-" + subject + " ?"
}
return verb_form + "-" + subject + " ?"
}