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
+262
View File
@@ -0,0 +1,262 @@
# -*- coding: utf-8 -*-
"""gen_elp_es.py — emit the ELP (.el) port artifacts for Spanish.
Mirrors gen_elp_en.py. Produces:
vocabulary-es.el real generated vocabulary in the established schema
[lemma, pos, form0, form1, form2, en_translation, semantic_hint]
(same schema as vocabulary-got.el / vocabulary-en.el;
UniMorph spa lineage).
Verbs : form0=present-ind-3sg form1=preterite-3sg form2=past-participle
Nouns : form0=singular form1=plural form2=REAL gender (m/f, lexicon)
Adjs : form0=masc-sg form1=fem-sg form2=masc-pl
lang_profile_es.el the Spanish profile with the flags the realizer keys on.
Every form is generated by morphology_es_full (real UniMorph lexicon, not
hand-typed), so the .el vocabulary is honest and reproduces the forms the
realizer used. CRITICAL (coordinator quality bar): noun gender in form2 is the
REAL per-lemma lexicon gender (N;FEM/MASC), NOT an ending heuristic — this is
what kills the 'el mano / la día' masculine-default error class.
"""
import morphology_es_full as M
from test_set_es import TESTS
from held_out_es import HELD
# ── core closed class + common content lemmas so the vocab is usable beyond the
# validated sentences ──────────────────────────────────────────────────────
_CORE_VERBS = ["ser", "estar", "haber", "tener", "hacer", "ir", "ver", "dar",
"saber", "poder", "querer", "venir", "decir", "poner", "salir",
"hablar", "comer", "vivir", "trabajar", "estudiar", "llegar",
"pasar", "deber", "parecer", "quedar", "creer", "dejar", "llevar",
"encontrar", "llamar", "pensar", "volver", "conocer", "sentir",
"contar", "empezar", "buscar", "esperar", "existir", "entrar",
"escribir", "perder", "producir", "recordar", "morir", "nacer",
"abrir", "escapar", "soñar", "amar", "caer", "leer", "oír"]
_CORE_NOUNS = ["tiempo", "persona", "año", "día", "mano", "mundo", "vida",
"hombre", "mujer", "parte", "casa", "país", "problema", "programa",
"tema", "mapa", "agua", "foto", "moto", "ciudad", "libertad",
"canción", "nación", "flor", "color", "amor", "señor", "viaje",
"paisaje", "gato", "perro", "libro", "mesa", "silla", "noche",
"luz", "voz", "pez", "raíz", "crisis", "sol", "luna", "mar",
"corazón", "flor", "árbol", "camino", "puerta", "ventana"]
_CORE_ADJS = ["bueno", "malo", "nuevo", "viejo", "grande", "pequeño", "alto",
"bajo", "largo", "corto", "feliz", "triste", "fácil", "difícil",
"rápido", "lento", "hermoso", "económico", "político", "social",
"azul", "rojo", "verde", "blanco", "negro", "español", "francés",
"inglés", "importante", "posible", "necesario", "trabajador"]
# closed-class function words. Contractions (del/al) and the government notes
# are the coordinator's quality bar (mandatory contraction; verb-prep govt).
_FUNCTION = [
# articles (gender/number agreement is in morphology; these are citation)
("el", "det", "el", "los", "m", "the", "definite article m.sg"),
("la", "det", "la", "las", "f", "the", "definite article f.sg"),
("un", "det", "un", "unos","m", "a", "indefinite article m.sg"),
("una", "det", "una", "unas","f", "a", "indefinite article f.sg"),
# MANDATORY CONTRACTIONS (prep + el) — del / al
("del", "contraction", "del", "", "", "of the", "de + el (mandatory contraction)"),
("al", "contraction", "al", "", "", "to the", "a + el (mandatory contraction)"),
# demonstratives
("este", "dem", "este", "estos", "m", "this", "proximal dem m"),
("esta", "dem", "esta", "estas", "f", "this", "proximal dem f"),
# negation (SACRED — polarity never dropped)
("no", "neg", "no", "", "", "not/no", "sentential negator (preverbal)"),
("ninguno", "det", "ningún", "ninguna", "", "none", "negative determiner (apocope ningún m.sg)"),
# conjunctions
("y", "conj", "y", "e", "", "and", "coordinator (e before i-/hi-)"),
("o", "conj", "o", "u", "", "or", "coordinator (u before o-/ho-)"),
("pero", "conj", "pero", "", "", "but", "adversative coordinator"),
("que", "conj", "que", "", "", "that", "complementizer / relative"),
("si", "conj", "si", "", "", "if", "conditional subordinator"),
("porque", "conj", "porque", "", "", "because", "causal subordinator"),
("cuando", "conj", "cuando", "", "", "when", "temporal subordinator"),
# prepositions (government: verbs select these; contraction with el applies to a/de)
("a", "prep", "a", "", "", "to", "dir-obj (personal a) / dative / allative; a+el=al"),
("de", "prep", "de", "", "", "of/from", "genitive/ablative government; de+el=del"),
("en", "prep", "en", "", "", "in/on", "locative"),
("con", "prep", "con", "", "", "with", "comitative"),
("por", "prep", "por", "", "", "by/for", "passive agent / cause"),
("para", "prep", "para", "", "", "for", "purpose/benefactive"),
("contra", "prep", "contra", "", "", "against", "adversative government (protestar contra)"),
("sin", "prep", "sin", "", "", "without", "privative"),
# subject pronouns
("yo", "pron", "yo", "me", "mi", "I", "1sg subj/obj/poss"),
("", "pron", "", "te", "tu", "you", "2sg informal"),
("usted", "pron", "usted", "lo", "su", "you", "2sg formal (3sg agreement)"),
("él", "pron", "él", "lo", "su", "he", "3sg m subj/DO-clitic/poss"),
("ella", "pron", "ella", "la", "su", "she", "3sg f subj/DO-clitic/poss"),
("nosotros", "pron", "nosotros", "nos", "nuestro", "we", "1pl"),
("vosotros", "pron", "vosotros", "os", "vuestro", "you", "2pl informal"),
("ellos", "pron", "ellos", "los", "su", "they", "3pl m"),
("ellas", "pron", "ellas", "las", "su", "they", "3pl f"),
# indirect-object clitics
("le", "clitic", "le", "les", "", "to-him/her", "dative clitic 3sg/3pl (->se before lo/la)"),
("se", "clitic", "se", "se", "", "himself/-self", "reflexive / spurious-se (le+lo->se lo)"),
]
def _walk_collect(spec, verbs, nouns, adjs):
"""Recursively collect verb/noun/adj lemmas from a semantic spec."""
if isinstance(spec, dict):
if spec.get("pred"):
verbs.add(spec["pred"])
if spec.get("noun"):
nouns.add(spec["noun"])
if spec.get("adj"):
adjs.add(spec["adj"])
if spec.get("superlative"):
adjs.add(spec["superlative"])
if spec.get("from_adj"):
adjs.add(spec["from_adj"])
# adjs: [{"lemma":..,"pos":..}] | ["lemma", ..]
for a in spec.get("adjs", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
for a in spec.get("adj_coord", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
if isinstance(spec.get("pcomp"), dict):
pc = spec["pcomp"]
if pc.get("adj"):
adjs.add(pc["adj"])
for a in pc.get("adj_coord", []) or []:
adjs.add(a["lemma"] if isinstance(a, dict) else a)
for v in spec.values():
_walk_collect(v, verbs, nouns, adjs)
elif isinstance(spec, list):
for it in spec:
_walk_collect(it, verbs, nouns, adjs)
def _collect_from_specs():
verbs, nouns, adjs = set(), set(), set()
for t in TESTS + HELD:
_walk_collect(t["spec"], verbs, nouns, adjs)
return verbs, nouns, adjs
def _esc(s):
return str(s).replace('"', '\\"')
def _row(fields):
return " (" + " ".join(f'"{_esc(f)}"' for f in fields) + ")"
def emit_vocabulary(path):
v_specs, n_specs, a_specs = _collect_from_specs()
verbs = sorted(set(_CORE_VERBS) | v_specs)
nouns = sorted(set(_CORE_NOUNS) | n_specs)
adjs = sorted(set(_CORE_ADJS) | a_specs)
lines = [
";;; vocabulary-es.el — Spanish vocabulary for ELP surface realization.",
";;; Schema: (lemma pos form0 form1 form2 en_translation semantic_hint)",
";;; Source: UniMorph Spanish (github.com/unimorph/spa, CC-BY-SA 3.0),",
";;; generated by gen_elp_es.py via morphology_es_full (real forms).",
";;; Verbs: form0=present-ind-3sg form1=preterite-3sg form2=past-participle",
";;; Nouns: form0=singular form1=plural form2=REAL gender (m/f, from lexicon —",
";;; NOT an ending heuristic; this is what kills 'el mano'/'la día' errors)",
";;; Adjs : form0=masc-sg form1=fem-sg form2=masc-pl",
"",
"(vocabulary-es",
"",
" ;; -- function / closed class (incl. mandatory contractions del/al) --------",
]
for f in _FUNCTION:
lines.append(_row(f))
lines.append("")
lines.append(" ;; -- verbs (form0=pres-3sg form1=pret-3sg form2=past-participle) -----------")
for lem in verbs:
f0, c0 = M.conjugate(lem, "ind", "present", "third", "singular")
f1, c1 = M.conjugate(lem, "ind", "preterite", "third", "singular")
pp, cp = M.participle(lem)
vclass = lem[-2:] if lem[-2:] in ("ar", "er", "ir") else "ar"
irr = "irregular" if (c0 == "lexicon" and pp in M._IRREG_PART.values()) or \
lem in ("ser", "estar", "ir", "haber", "tener", "hacer", "ver",
"dar", "saber", "poder", "querer", "venir", "decir",
"poner", "salir") else "regular"
lines.append(_row([lem, "verb", f0, f1, pp, lem, vclass + "/" + irr]))
lines.append("")
lines.append(" ;; -- nouns (form0=sg form1=pl form2=REAL gender m/f) ----------------------")
for lem in nouns:
sg, _ = M.inflect_noun(lem, "singular")
pl, _ = M.inflect_noun(lem, "plural")
g = M.noun_gender(lem)
# honesty flag: did gender come from the lexicon, or a heuristic fallback?
src = "lexicon" if (lem in M._NOUNS and M._NOUNS[lem].get("g")) else "heuristic"
lines.append(_row([lem, "noun", sg, pl, g, lem, "gender:" + src]))
lines.append("")
lines.append(" ;; -- adjectives (form0=masc-sg form1=fem-sg form2=masc-pl) ----------------")
for lem in adjs:
m_sg, _ = M.inflect_adj(lem, "m", "singular")
f_sg, _ = M.inflect_adj(lem, "f", "singular")
m_pl, _ = M.inflect_adj(lem, "m", "plural")
src = "lexicon" if lem in M._ADJS else "rule"
lines.append(_row([lem, "adj", m_sg, f_sg, m_pl, lem, src]))
lines.append("")
lines.append(")")
with open(path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
return len(_FUNCTION) + len(verbs) + len(nouns) + len(adjs), len(verbs), len(nouns), len(adjs)
LANG_PROFILE = ''';;; lang_profile_es.el — Spanish language profile for ELP.
;;; Keys the realizer's construction switches. Mirrors lang_profile_en / _pt.
(lang_profile_es
(language "Spanish")
(iso639 "es")
(family "Romance")
;; -- core typology flags -------------------------------------------------
(pro-drop yes) ; subjects routinely dropped; agreement carries person
(obligatory-subject no)
(grammatical-gender yes) ; m/f on every noun; article+adjective AGREE
(gender-source lexicon); REAL per-noun gender from UniMorph — NOT a heuristic
(do-support no)
(subject-aux-inversion no) ; questions by intonation/punctuation, not inversion
(question-strategy intonation)
(article-selection "el/la/los/las un/una/unos/unas")
(stressed-a-rule yes) ; fem sg noun in stressed a-/ha- takes el/un (el agua)
(adjective-position postnominal) ; default post; a few prenominal + apocope
(adjective-agreement "gender+number")
(question-punct inverted) ; opening ¿ ¡ required
;; -- MANDATORY CONTRACTIONS (coordinator quality bar) --------------------
(contractions ((de el "del") (a el "al")))
(contraction-mandatory yes) ; 'de el'/'a el' MUST surface as del/al
;; -- verb / aspect system ------------------------------------------------
(verb-classes (ar er ir))
(tenses (present preterite imperfect future conditional))
(moods (ind sbjv imp))
(finite-agreement "person+number (6 slots)")
(perfect-aux "haber") ; haber + past participle (invariant -o)
(progressive-aux "estar") ; estar + gerund
(passive-aux "ser") ; ser + participle (agrees) + por-agent
(copula-split "ser/estar") ; permanent vs stage-level
(future "infinitive + é/ás/á/emos/éis/án")
;; -- clitics / government ------------------------------------------------
(object-clitics yes) ; me te lo la le nos os los las; proclisis/enclisis
(clitic-order "se II I III (le+lo -> se lo)")
(enclisis "imperative/infinitive/gerund + accent repair (dá+me+lo->dámelo)")
(verb-prep-government yes) ; verbs select prep (protestar+contra, escapar+de)
;; -- SACRED safety bar (shared with en/pt) -------------------------------
(negation-faithful yes)) ; polarity never dropped/inverted; unplaceable -> FLAG
'''
if __name__ == "__main__":
import sys
voc_path = sys.argv[1] if len(sys.argv) > 1 else "vocabulary-es.el"
lp_path = sys.argv[2] if len(sys.argv) > 2 else "lang_profile_es.el"
total, nv, nn, na = emit_vocabulary(voc_path)
with open(lp_path, "w", encoding="utf-8") as fh:
fh.write(LANG_PROFILE)
print(f"wrote {voc_path} ({total} entries: {len(_FUNCTION)} fn, {nv} verbs, {nn} nouns, {na} adjs)")
print(f"wrote {lp_path}")
print("lexicon:", M.lexicon_stats())
+100
View File
@@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
"""gen_parity_es.py — emit an El parity program that checks the .el Spanish
morphology against the validated Python (UniMorph-backed) realizer.
Python is the ORACLE. For every lemma in the held-out inventory we embed the
Python-produced form, call the corresponding .el function, and the El program
prints PASS/FAIL per category. Aggregation is done in bash (grep -c), so no
El-side mutable counters are needed.
Categories:
Vpres verb present-ind-3sg es_conjugate(v,present,third,singular)
Vpret verb preterite-3sg es_conjugate(v,past,third,singular)
Vpart past participle es_participle(v)
Npl noun plural es_pluralize(n)
Gheur noun gender HEURISTIC es_gender(n) [exposes the bug]
Aheur def article via heuristic es_agree_article(n,true,sg) [inherits the bug]
Avocab def article via REAL g es_article_for_gender(realg,...) [the fix]
Ampl adj masc-plural es_inflect_adj(a,m,plural)
Afsg adj fem-singular es_inflect_adj(a,f,singular)
Ctr contraction del/al es_contract(prep, np)
"""
import morphology_es_full as M
import realizer_es as R
from gen_elp_es import _collect_from_specs, _CORE_VERBS, _CORE_NOUNS, _CORE_ADJS
def _esc(s):
return str(s).replace('\\', '\\\\').replace('"', '\\"')
def _check(cat, lemma, el_call, expected):
return (f' es_check("{cat}", "{_esc(lemma)}", {el_call}, "{_esc(expected)}")')
def main(out_path):
v_specs, n_specs, a_specs = _collect_from_specs()
verbs = sorted(set(_CORE_VERBS) | v_specs)
nouns = sorted(set(_CORE_NOUNS) | n_specs)
adjs = sorted(set(_CORE_ADJS) | a_specs)
lines = []
lines.append("// gen'd parity checks — Python oracle embedded, El functions called.")
lines.append("fn es_check(cat: String, lemma: String, got: String, exp: String) {")
lines.append(' if str_eq(got, exp) {')
lines.append(' println("PASS " + cat)')
lines.append(' } else {')
lines.append(' println("FAIL " + cat + " " + lemma + " got=" + got + " exp=" + exp)')
lines.append(' }')
lines.append("}")
lines.append("")
lines.append("fn es_parity() {")
# verbs
for v in verbs:
p0 = M.conjugate(v, "ind", "present", "third", "singular")[0]
p1 = M.conjugate(v, "ind", "preterite", "third", "singular")[0]
pp = M.participle(v)[0]
lines.append(_check("Vpres", v, f'es_conjugate("{_esc(v)}", "present", "third", "singular")', p0))
lines.append(_check("Vpret", v, f'es_conjugate("{_esc(v)}", "past", "third", "singular")', p1))
lines.append(_check("Vpart", v, f'es_participle("{_esc(v)}")', pp))
# nouns
for n in nouns:
pl = M.inflect_noun(n, "plural")[0]
g = M.noun_gender(n) # REAL lexicon gender
art = R._article(g, "singular", "def", n) # oracle article from real gender
lines.append(_check("Npl", n, f'es_pluralize("{_esc(n)}")', pl))
lines.append(_check("Gheur", n, f'es_gender("{_esc(n)}")', g))
lines.append(_check("Aheur", n, f'es_agree_article("{_esc(n)}", "true", "singular")', art))
lines.append(_check("Avocab", n, f'es_article_for_gender("{_esc(g)}", "{_esc(n)}", "true", "singular")', art))
# adjectives
for a in adjs:
mpl = M.inflect_adj(a, "m", "plural")[0]
fsg = M.inflect_adj(a, "f", "singular")[0]
lines.append(_check("Ampl", a, f'es_inflect_adj("{_esc(a)}", "m", "plural")', mpl))
lines.append(_check("Afsg", a, f'es_inflect_adj("{_esc(a)}", "f", "singular")', fsg))
# contractions (mandatory)
ctr_cases = [("de", "el día"), ("a", "el hombre"), ("de", "el mundo"),
("a", "el país"), ("en", "el mar"), ("de", "el año"),
("a", "la casa"), ("de", "la ciudad")]
for prep, np in ctr_cases:
exp = R._contract(prep, np)
lines.append(_check("Ctr", prep + "+" + np, f'es_contract("{_esc(prep)}", "{_esc(np)}")', exp))
lines.append("}")
lines.append("")
lines.append("fn main() {")
lines.append(" es_parity()")
lines.append("}")
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(lines) + "\n")
print(f"wrote {out_path} ({len(verbs)} verbs, {len(nouns)} nouns, {len(adjs)} adjs)")
if __name__ == "__main__":
import sys
main(sys.argv[1] if len(sys.argv) > 1 else "parity_es_checks.el")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff