From 0ae33c0f3bbbbe5a6011e99ca2cb6f25cb431522 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 13 Aug 2026 15:39:49 -0500 Subject: [PATCH] elp(realizer): close subordinate-clause round-trip + silent-e/doubling lemmatizer + verb-final object bug + ES/PT closed-class verb guard - realizer now carries the subordinate clause verbatim (subord_text slot): the 5th English acceptance sentence is byte-identical through parse->realize->reparse. - English -ed/-ing lemmatizer restores silent-e (loved->love) and collapses inflectional doubling (stopped->stop), inverting en_verb_past(). - parse_spec_lang: a sentence-final main verb no longer bleeds into the object slot (cstart advanced to verb+1), so intransitives round-trip. - cp_rom_is_verb rejects closed-class words (prep/det/pron/aux/neg) before the ending-only test, killing the 'para'/determiner misfires. EN telephone gate 5/5 (now byte-identical 5/5), Romance gate 6/6. --- elp/src/comprehend.el | 87 +++++++++++++++++++++++++++++++++++++++++-- elp/src/realizer.el | 17 +++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/elp/src/comprehend.el b/elp/src/comprehend.el index df39ef8..18b75c5 100644 --- a/elp/src/comprehend.el +++ b/elp/src/comprehend.el @@ -330,12 +330,71 @@ fn cp_irr2(surface: String) -> [String] { } // cp_reg_verb(surface) -> [lemma, tense, aspect] by suffix stripping (regular). +// ── English silent-e / consonant-doubling inverse (past-participle lemmatizer) ── +// +// Stripping "-ed" naively over-truncates two regular classes: +// • silent-e verbs ("loved" -> "lov" instead of "love") +// • doubled-final ("stopped" -> "stopp" instead of "stop") +// These helpers invert en_verb_past() so the round-trip closes. The rules are +// the safe, high-confidence subset; residual orthographic edge cases (e.g. bare +// "ebb"/"arc") are rare and left to the runtime vocabulary confirmer. + +fn cp_e_vowel(c: String) -> Bool { + if str_eq(c, "a") { return true } + if str_eq(c, "e") { return true } + if str_eq(c, "i") { return true } + if str_eq(c, "o") { return true } + if str_eq(c, "u") { return true } + return false +} + +// consonants that double under inflection (CVC rule): b d g l m n p r t +fn cp_e_doubling(c: String) -> Bool { + if str_eq(c, "b") { return true } + if str_eq(c, "d") { return true } + if str_eq(c, "g") { return true } + if str_eq(c, "l") { return true } + if str_eq(c, "m") { return true } + if str_eq(c, "n") { return true } + if str_eq(c, "p") { return true } + if str_eq(c, "r") { return true } + if str_eq(c, "t") { return true } + return false +} + +// restore the base after "-ed"/"-ing" stripping produced `stem`. +fn cp_e_restore(stem: String) -> String { + let m: Int = str_len(stem) + if m < 2 { return stem } + let last: String = str_slice(stem, m - 1, m) + let prev: String = str_slice(stem, m - 2, m - 1) + // inflectional doubled consonant -> collapse (stopped->stop, planned->plan) + if str_eq(last, prev) { + if cp_e_doubling(last) { return str_slice(stem, 0, m - 1) } + } + // silent-e restoration + if str_eq(last, "v") { return stem + "e" } + if str_eq(last, "u") { return stem + "e" } + let ctx: Bool = false + if cp_e_vowel(prev) { let ctx = true } + if str_eq(prev, "l") { let ctx = true } + if str_eq(prev, "r") { let ctx = true } + if str_eq(prev, "n") { let ctx = true } + if ctx { + if str_eq(last, "c") { return stem + "e" } + if str_eq(last, "g") { return stem + "e" } + if str_eq(last, "s") { return stem + "e" } + if str_eq(last, "z") { return stem + "e" } + } + return stem +} + fn cp_reg_verb(w: String) -> [String] { let n: Int = str_len(w) let r: [String] = native_list_empty() if str_ends_with(w, "ing") { if n > 4 { - let lemma: String = str_slice(w, 0, n - 3) + let lemma: String = cp_e_restore(str_slice(w, 0, n - 3)) let r = native_list_append(r, lemma) let r = native_list_append(r, "present") let r = native_list_append(r, "progressive") @@ -351,7 +410,7 @@ fn cp_reg_verb(w: String) -> [String] { } if str_ends_with(w, "ed") { if n > 2 { - let lemma: String = str_slice(w, 0, n - 2) + let lemma: String = cp_e_restore(str_slice(w, 0, n - 2)) let r = native_list_append(r, lemma) let r = native_list_append(r, "past") let r = native_list_append(r, "simple") @@ -617,6 +676,15 @@ fn cp_rom_aux(w: String) -> Bool { fn cp_rom_is_verb(w: String) -> Bool { let n: Int = str_len(w) if n < 3 { return false } + // Closed-class words are never finite verbs — reject them before the + // (ambiguous) inflectional-ending test. This kills the ending-only + // misfires on prepositions ("para", "sobre", "entre", "desde", "hasta") + // and on determiners / subject pronouns that happen to end in -a/-e/-o. + if cp_rom_prep(w) { return false } + if cp_rom_det(w) { return false } + if cp_rom_aux(w) { return false } + if cp_is_negation(w) { return false } + if !str_eq(cp_rom_pron(w), "") { return false } if str_ends_with(w, "ar") { return true } if str_ends_with(w, "er") { return true } if str_ends_with(w, "ir") { return true } @@ -969,7 +1037,6 @@ fn parse_spec_lang(text: String, lang: String) -> [String] { while j < m_end { let w: String = native_list_get(toks, j) if seen_v { - let cstart = j let j = m_end } else { if cp_is_negation(w) { @@ -978,8 +1045,12 @@ fn parse_spec_lang(text: String, lang: String) -> [String] { if cp_is_aux(w) { let j = j + 1 } else { + // main verb consumed — complements begin at the NEXT token. + // (Setting cstart here, not on a later iteration, is what makes + // a sentence-final verb yield no phantom object.) let seen_v = true - let j = j + 1 + let cstart = j + 1 + let j = m_end } } } @@ -1044,10 +1115,17 @@ fn parse_spec_lang(text: String, lang: String) -> [String] { } // subordinate clause: conj + (lightweight) predicate recovery. + // subord_conj the subordinating conjunction ("because", "if", ...) + // subord_pred the recovered predicate lemma of the subordinate clause + // subord_text the SURFACE of the subordinate clause AFTER the conjunction, + // carried verbatim so realization is byte-complete (the clause + // is preserved, not dropped — SACRED completeness of meaning). let subord_conj: String = "" let subord_pred: String = "" + let subord_text: String = "" if m_end < n { let subord_conj = native_list_get(toks, m_end) + let subord_text = cp_join_range(toks, m_end + 1, n) // find the subordinate predicate: first aux/verb after the conjunction. let s: Int = m_end + 1 let sfound: Bool = false @@ -1095,6 +1173,7 @@ fn parse_spec_lang(text: String, lang: String) -> [String] { let spec = native_list_append(spec, "neg_word"); let spec = native_list_append(spec, neg_word) let spec = native_list_append(spec, "subord_conj"); let spec = native_list_append(spec, subord_conj) let spec = native_list_append(spec, "subord_pred"); let spec = native_list_append(spec, subord_pred) + let spec = native_list_append(spec, "subord_text"); let spec = native_list_append(spec, subord_text) let spec = native_list_append(spec, "lang"); let spec = native_list_append(spec, lang) return spec } diff --git a/elp/src/realizer.el b/elp/src/realizer.el index 0dc655e..9ef642b 100644 --- a/elp/src/realizer.el +++ b/elp/src/realizer.el @@ -339,6 +339,19 @@ fn realize_lang(form: [String], profile: [String]) -> String { let iobj: String = slots_get(form, "iobj") let code: String = lang_get(profile, "code") + // Subordinate clause tail (SACRED completeness — the clause is carried, never + // dropped): " ", e.g. "because he was a monster". + let subord_conj: String = slots_get(form, "subord_conj") + let subord_text: String = slots_get(form, "subord_text") + let subord_tail: String = "" + if !str_eq(subord_conj, "") { + if !str_eq(subord_text, "") { + let subord_tail = subord_conj + " " + subord_text + } else { + let subord_tail = subord_conj + } + } + // Negative polarity: SACRED — never dropped. if str_eq(polarity, "neg") { if str_eq(code, "en") { @@ -359,6 +372,7 @@ fn realize_lang(form: [String], profile: [String]) -> String { let parts = native_list_append(parts, iobj) } if !str_eq(location, "") { let parts = native_list_append(parts, location) } + if !str_eq(subord_tail, "") { let parts = native_list_append(parts, subord_tail) } let sentence: String = str_join(parts, " ") return add_punct(capitalize_first(sentence), "assert") } @@ -380,6 +394,9 @@ fn realize_lang(form: [String], profile: [String]) -> String { if !str_eq(location, "") { let parts = native_list_append(parts, location) } + if !str_eq(subord_tail, "") { + let parts = native_list_append(parts, subord_tail) + } let sentence: String = str_join(parts, " ") return add_punct(capitalize_first(sentence), "assert") }