stage(elp-es): emit vocabulary-es.el + lang_profile_es.el; extend morphology-es.el; parity harness

Port the validated sandbox Spanish realizer into the ELP .el runtime (STAGE only).
- vocabulary-es.el: 342 entries generated from UniMorph via morphology_es_full
  (real forms; nouns carry REAL per-lemma lexicon gender in form2 — kills the
  el-mano/la-dia masculine-default class).
- lang_profile_es.el: Spanish typology flags incl. mandatory contractions + govt.
- morphology-es.el: add es_participle, es_inflect_adj, es_contract,
  es_article_for_gender; accent-correct plural/preterite/participle.
- parity harness: 90.1% morphology parity vs Python oracle; gender heuristic
  73.6% vs vocab-real 100%; contraction 100%.
This commit is contained in:
claude
2026-08-13 09:49:20 -05:00
parent ba6e36c3f7
commit c62ea3383c
7 changed files with 2995 additions and 10 deletions
+171 -10
View File
@@ -54,6 +54,16 @@ fn es_str_last3(s: String) -> String {
// Spanish verbs fall into three conjugation classes defined by the infinitive
// ending: -ar, -er, -ir. The stem is the infinitive minus those two characters.
// Strong-vowel-final test (a/e/o) used for orthographic y-insertion and the
// accented -ído participle (caer->caído/cayó; but ui/iu diphthongs stay plain).
fn es_strong_vowel_final(s: String) -> Bool {
let c: String = es_str_last_char(s)
if str_eq(c, "a") { return true }
if str_eq(c, "e") { return true }
if str_eq(c, "o") { return true }
return false
}
fn es_verb_class(base: String) -> String {
if es_str_ends(base, "ar") { return "ar" }
if es_str_ends(base, "er") { return "er" }
@@ -453,12 +463,18 @@ fn es_regular_preterite(stem: String, vclass: String, slot: Int) -> String {
if slot == 4 { return stem + "asteis" }
return stem + "aron"
}
// -er and -ir share the same preterite endings
// -er and -ir share the same preterite endings.
// Orthographic rule: a vowel-final stem takes -yó/-yeron (caer->cayó,
// leer->leyó, creer->creyó) since -ió after a vowel becomes -yó.
if slot == 0 { return stem + "í" }
if slot == 1 { return stem + "iste" }
if slot == 2 { return stem + "" }
if slot == 2 {
if es_strong_vowel_final(stem) { return stem + "" }
return stem + ""
}
if slot == 3 { return stem + "imos" }
if slot == 4 { return stem + "isteis" }
if es_strong_vowel_final(stem) { return stem + "yeron" }
return stem + "ieron"
}
@@ -640,17 +656,35 @@ fn es_pluralize(noun: String) -> String {
if !str_eq(inv, "") {
return inv
}
let last: String = es_str_last_char(noun)
// Oxytone nouns ending accented-vowel + n LOSE the written accent in the
// plural (canción->canciones, razón->razones, jardín->jardines).
// NOTE El strings are byte-indexed; "ón"/"án"/... are 3 bytes (accent=2 +n).
if es_str_ends(noun, "ón") { return es_str_drop_last(noun, 3) + "ones" }
if es_str_ends(noun, "án") { return es_str_drop_last(noun, 3) + "anes" }
if es_str_ends(noun, "én") { return es_str_drop_last(noun, 3) + "enes" }
if es_str_ends(noun, "ín") { return es_str_drop_last(noun, 3) + "ines" }
if es_str_ends(noun, "ún") { return es_str_drop_last(noun, 3) + "unes" }
// Oxytone accented-vowel + s also loses the accent (francés->franceses,
// inglés->ingleses). í/ú stay (país->países via the consonant rule).
if es_str_ends(noun, "és") { return es_str_drop_last(noun, 3) + "eses" }
if es_str_ends(noun, "ás") { return es_str_drop_last(noun, 3) + "ases" }
if es_str_ends(noun, "ós") { return es_str_drop_last(noun, 3) + "oses" }
// Ends in -z: replace with -ces
if str_eq(last, "z") {
if es_str_ends(noun, "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" }
// Stressed final vowel: á/é/ó -> +s (café->cafés); í/ú -> +es (rubí->rubíes)
if es_str_ends(noun, "á") { return noun + "s" }
if es_str_ends(noun, "é") { return noun + "s" }
if es_str_ends(noun, "ó") { return noun + "s" }
if es_str_ends(noun, "í") { return noun + "es" }
if es_str_ends(noun, "ú") { return noun + "es" }
// Plain final vowel: add -s
if es_str_ends(noun, "a") { return noun + "s" }
if es_str_ends(noun, "e") { return noun + "s" }
if es_str_ends(noun, "i") { return noun + "s" }
if es_str_ends(noun, "o") { return noun + "s" }
if es_str_ends(noun, "u") { return noun + "s" }
// Ends in consonant (including -s for stressed words like autobús): add -es
return noun + "es"
}
@@ -687,6 +721,37 @@ fn es_starts_with_stressed_a(noun: String) -> Bool {
return false
}
// es_article_for_gender: the article logic, given gender EXPLICITLY.
// The realizer should call this with the REAL per-noun gender from
// vocabulary-es.el (form2), NOT the es_gender heuristic that is what
// eliminates the 'el mano' / 'la día' masculine-default error class.
fn es_article_for_gender(gender: String, noun: String, definite: String, number: String) -> String {
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"
}
if str_eq(gender, "f") {
if es_starts_with_stressed_a(noun) { return "el" }
return "la"
}
return "el"
}
if is_plural {
if str_eq(gender, "f") { return "unas" }
return "unos"
}
if str_eq(gender, "f") {
if es_starts_with_stressed_a(noun) { return "un" }
return "una"
}
return "un"
}
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")
@@ -714,3 +779,99 @@ fn es_agree_article(noun: String, definite: String, number: String) -> String {
if str_eq(gender, "f") { return "una" }
return "un"
}
// Past participle (compound tenses: haber + participle)
//
// Irregular participle table (transcribed from morphology_es_full._IRREG_PART),
// then the regular rule: -ar -> -ado, -er/-ir -> -ido.
fn es_participle(verb: String) -> String {
if str_eq(verb, "escribir") { return "escrito" }
if str_eq(verb, "describir") { return "descrito" }
if str_eq(verb, "abrir") { return "abierto" }
if str_eq(verb, "cubrir") { return "cubierto" }
if str_eq(verb, "descubrir") { return "descubierto" }
if str_eq(verb, "morir") { return "muerto" }
if str_eq(verb, "poner") { return "puesto" }
if str_eq(verb, "ver") { return "visto" }
if str_eq(verb, "volver") { return "vuelto" }
if str_eq(verb, "devolver") { return "devuelto" }
if str_eq(verb, "hacer") { return "hecho" }
if str_eq(verb, "deshacer") { return "deshecho" }
if str_eq(verb, "decir") { return "dicho" }
if str_eq(verb, "romper") { return "roto" }
if str_eq(verb, "resolver") { return "resuelto" }
if str_eq(verb, "freír") { return "frito" }
if str_eq(verb, "imprimir") { return "impreso" }
if str_eq(verb, "satisfacer") { return "satisfecho" }
if str_eq(verb, "prever") { return "previsto" }
if str_eq(verb, "revolver") { return "revuelto" }
// Accented -ír infinitives (oír->oído, sonreír->sonreído): "ír" is 3 bytes.
if es_str_ends(verb, "ír") { return es_str_drop_last(verb, 3) + "ído" }
if es_str_ends(verb, "ar") { return es_str_drop_last(verb, 2) + "ado" }
// -er/-ir: a strong-vowel stem takes the accented -ído (caer->caído,
// leer->leído, poseer->poseído); consonant stems stay -ido; ui/iu
// diphthongs (construir->construido) stay plain.
if es_str_ends(verb, "er") {
let st: String = es_str_drop_last(verb, 2)
if es_strong_vowel_final(st) { return st + "ído" }
return st + "ido"
}
if es_str_ends(verb, "ir") {
let st: String = es_str_drop_last(verb, 2)
if es_strong_vowel_final(st) { return st + "ído" }
return st + "ido"
}
return verb
}
// Adjective agreement (gender + number)
//
// Rule-based agreement mirroring morphology_es_full.inflect_adj (rule path):
// feminine: -o -> -a; nationality/-dor exceptions add -a; else invariant
// plural: reuse es_pluralize on the agreed singular
// (Lexicon-backed adjectives in Python may differ; those divergences are what
// the parity check surfaces.)
fn es_adj_feminine(lemma: String) -> String {
if str_eq(lemma, "español") { return "española" }
if str_eq(lemma, "francés") { return "francesa" }
if str_eq(lemma, "inglés") { return "inglesa" }
if str_eq(lemma, "alemán") { return "alemana" }
if str_eq(lemma, "trabajador") { return "trabajadora" }
if str_eq(lemma, "hablador") { return "habladora" }
if str_eq(lemma, "encantador") { return "encantadora" }
if es_str_ends(lemma, "o") { return es_str_drop_last(lemma, 1) + "a" }
return lemma
}
fn es_inflect_adj(lemma: String, gender: String, number: String) -> String {
if str_eq(gender, "f") {
let fem: String = es_adj_feminine(lemma)
if str_eq(number, "plural") { return es_pluralize(fem) }
return fem
}
// masculine (citation)
if str_eq(number, "plural") { return es_pluralize(lemma) }
return lemma
}
// Mandatory preposition + article contraction (de+el->del, a+el->al)
//
// Transcribed from realizer_es._contract. np_text is the already-realized NP
// beginning with "el " when the contraction fires.
fn es_starts_el(np_text: String) -> Bool {
let n: Int = str_len(np_text)
if n < 3 { return false }
return str_eq(str_slice(np_text, 0, 3), "el ")
}
fn es_contract(prep: String, np_text: String) -> String {
if es_starts_el(np_text) {
let rest: String = str_slice(np_text, 3, str_len(np_text))
if str_eq(prep, "de") { return "del " + rest }
if str_eq(prep, "a") { return "al " + rest }
}
return prep + " " + np_text
}