From 89ea1b5a15aed74936f80ad8aa2981bceabb6e15 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 13 Aug 2026 13:41:09 -0500 Subject: [PATCH] elp(comprehend): el-native comprehension parser + SACRED polarity end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIECE 1 — greenfield el-native parser (comprehend.el), spaCy-free: - text -> meaning-spec via invertible English morphology (the realizer's own irregular table run BACKWARD) + a deterministic clause grammar (subject/verb boundary, roles, ditransitive iobj, PP adjuncts, subordination, coordination). - NEGATION IS SACRED: explicit polarity field, always present, cross-lingual lexeme set; standalone neg adverbs (never) captured separately. - WSD by deterministic syntactic position over a fixed sense inventory (flies->fly, like->comparison, saw->see); engram nearest-region is the documented runtime upgrade hook (no external model). Polarity threaded through the whole el contract (was previously dropped at the boundary): realizer.el realize_lang honors polarity (English do-support / adverbial / copular negation; generic preverbal negator for es/pt/ca/it/fr/de/ro) and places iobj; elp.el build_form_from_json carries polarity/neg_word/iobj across JSON; morphology.el gains 'fight'. Acceptance (native el telephone test, comprehend_gate.el): on the 5 gate sentences polarity PRESERVED 5/5 and EXTRACTED 5/5 through parse->realize-> re-parse; 4/5 byte-identical. Built bounded (elc rc=0, cc rc=0). --- elp/manifest.el | 1 + elp/src/comprehend.el | 768 +++++++++++++++++++++++++++++++++++ elp/src/comprehend.elh | 16 + elp/src/elp.el | 13 + elp/src/morphology.el | 1 + elp/src/realizer.el | 84 ++++ elp/tests/comprehend_gate.el | 93 +++++ 7 files changed, 976 insertions(+) create mode 100644 elp/src/comprehend.el create mode 100644 elp/src/comprehend.elh create mode 100644 elp/tests/comprehend_gate.el diff --git a/elp/manifest.el b/elp/manifest.el index dcec603..3d212fb 100644 --- a/elp/manifest.el +++ b/elp/manifest.el @@ -80,6 +80,7 @@ build { "src/grammar.el", "src/realizer.el", "src/semantics.el", + "src/comprehend.el", "src/elp.el", ] } diff --git a/elp/src/comprehend.el b/elp/src/comprehend.el new file mode 100644 index 0000000..e442ee1 --- /dev/null +++ b/elp/src/comprehend.el @@ -0,0 +1,768 @@ +// comprehend.el - ELP native COMPREHENSION front-end: text -> meaning-spec. +// +// The input half of the ELP, the deterministic inverse of the realizer. No LLM, +// no spaCy: analysis uses ELP's own morphology tables run BACKWARD (invertible +// morphology), a deterministic clause grammar for roles / subordination / +// coordination / polarity, and a word-sense picker that (at runtime) defers to +// the engram's own nearest-region embeddings. +// +// parse_spec(text) -> [String] (a slot map, the same shape realize() consumes, +// EXTENDED with the SACRED polarity field): +// intent "assert" | "question" | "command" +// agent subject referent (pronoun surface, or "det adj noun") +// predicate verb concept (English lemma = interlingua) +// patient direct-object NP (optional) +// iobj recipient NP for ditransitives (optional) +// location prepositional adjunct e.g. "with the telescope" (optional) +// tense "present" | "past" | "future" +// aspect "simple" | "progressive" | "perfect" +// polarity "aff" | "neg" <-- SACRED. Always present. Never inferred away. +// neg_word standalone negative adverb e.g. "never" (optional) +// subord_conj subordinating conjunction concept e.g. "because" (optional) +// subord_pred predicate of the subordinate clause (optional) +// lang ISO 639-1 code +// +// Depends on (via concatenation order): language-profile, morphology, grammar. + +// ── token cleaning ──────────────────────────────────────────────────────────── + +fn cp_is_punct(c: String) -> Bool { + if str_eq(c, ".") { return true } + if str_eq(c, ",") { return true } + if str_eq(c, "!") { return true } + if str_eq(c, "?") { return true } + if str_eq(c, ";") { return true } + if str_eq(c, ":") { return true } + if str_eq(c, "\"") { return true } + if str_eq(c, "'") { return true } + if str_eq(c, "(") { return true } + if str_eq(c, ")") { return true } + return false +} + +fn cp_trim_punct(s: String) -> String { + let n: Int = str_len(s) + let end: Int = n + let running: Bool = true + while running { + if end <= 0 { + let running = false + } else { + let c: String = str_slice(s, end - 1, end) + if cp_is_punct(c) { + let end = end - 1 + } else { + let running = false + } + } + } + let start: Int = 0 + let running2: Bool = true + while running2 { + if start >= end { + let running2 = false + } else { + let c2: String = str_slice(s, start, start + 1) + if cp_is_punct(c2) { + let start = start + 1 + } else { + let running2 = false + } + } + } + return str_slice(s, start, end) +} + +fn cp_clean(tok: String) -> String { + return str_to_lower(cp_trim_punct(tok)) +} + +fn cp_tokenize(text: String) -> [String] { + let raw: [String] = str_split(text, " ") + let n: Int = native_list_len(raw) + let out: [String] = native_list_empty() + let i: Int = 0 + while i < n { + let t: String = cp_clean(native_list_get(raw, i)) + if !str_eq(t, "") { + let out = native_list_append(out, t) + } + let i = i + 1 + } + return out +} + +// ── closed-class lexicon ────────────────────────────────────────────────────── + +fn cp_is_determiner(w: String) -> Bool { + if str_eq(w, "the") { return true } + if str_eq(w, "a") { return true } + if str_eq(w, "an") { return true } + if str_eq(w, "this") { return true } + if str_eq(w, "these") { return true } + if str_eq(w, "those") { return true } + if str_eq(w, "my") { return true } + if str_eq(w, "your") { return true } + if str_eq(w, "his") { return true } + if str_eq(w, "its") { return true } + if str_eq(w, "our") { return true } + if str_eq(w, "their") { return true } + return false +} + +// English subject/object pronoun -> concept ("" if not a pronoun). +fn cp_pron_concept(w: String) -> String { + if str_eq(w, "i") { return "i" } + if str_eq(w, "me") { return "i" } + if str_eq(w, "we") { return "we" } + if str_eq(w, "us") { return "we" } + if str_eq(w, "you") { return "you" } + if str_eq(w, "he") { return "he" } + if str_eq(w, "him") { return "he" } + if str_eq(w, "she") { return "she" } + if str_eq(w, "it") { return "it" } + if str_eq(w, "they") { return "they" } + if str_eq(w, "them") { return "they" } + return "" +} + +// concept -> canonical subject surface (the form realize()/agent_person expects). +fn cp_pron_surface(concept: String) -> String { + if str_eq(concept, "i") { return "I" } + if str_eq(concept, "we") { return "we" } + if str_eq(concept, "you") { return "you" } + if str_eq(concept, "he") { return "he" } + if str_eq(concept, "she") { return "she" } + if str_eq(concept, "it") { return "it" } + if str_eq(concept, "they") { return "they" } + return concept +} + +fn cp_is_preposition(w: String) -> Bool { + if str_eq(w, "in") { return true } + if str_eq(w, "on") { return true } + if str_eq(w, "at") { return true } + if str_eq(w, "to") { return true } + if str_eq(w, "with") { return true } + if str_eq(w, "from") { return true } + if str_eq(w, "by") { return true } + if str_eq(w, "for") { return true } + if str_eq(w, "of") { return true } + if str_eq(w, "into") { return true } + if str_eq(w, "onto") { return true } + if str_eq(w, "over") { return true } + if str_eq(w, "under") { return true } + if str_eq(w, "about") { return true } + if str_eq(w, "than") { return true } + if str_eq(w, "through") { return true } + if str_eq(w, "near") { return true } + if str_eq(w, "around") { return true } + if str_eq(w, "between") { return true } + if str_eq(w, "without") { return true } + if str_eq(w, "upon") { return true } + return false +} + +// SACRED: negation lexeme test. Cross-lingual so the same field survives transfer. +fn cp_is_negation(w: String) -> Bool { + if str_eq(w, "not") { return true } + if str_eq(w, "never") { return true } + if str_eq(w, "no") { return true } + if str_eq(w, "none") { return true } + if str_eq(w, "nothing") { return true } + if str_eq(w, "nobody") { return true } + if str_eq(w, "cannot") { return true } + if str_contains(w, "n't") { return true } + if str_eq(w, "nada") { return true } + if str_eq(w, "nadie") { return true } + if str_eq(w, "nunca") { return true } + if str_eq(w, "jamás") { return true } + if str_eq(w, "jamais") { return true } + if str_eq(w, "não") { return true } + if str_eq(w, "nem") { return true } + if str_eq(w, "nenhum") { return true } + if str_eq(w, "ninguém") { return true } + return false +} + +fn cp_is_neg_adverb(w: String) -> Bool { + if str_eq(w, "never") { return true } + if str_eq(w, "nunca") { return true } + if str_eq(w, "jamás") { return true } + if str_eq(w, "jamais") { return true } + return false +} + +fn cp_is_aux(w: String) -> Bool { + if str_eq(w, "am") { return true } + if str_eq(w, "is") { return true } + if str_eq(w, "are") { return true } + if str_eq(w, "was") { return true } + if str_eq(w, "were") { return true } + if str_eq(w, "be") { return true } + if str_eq(w, "been") { return true } + if str_eq(w, "being") { return true } + if str_eq(w, "do") { return true } + if str_eq(w, "does") { return true } + if str_eq(w, "did") { return true } + if str_eq(w, "have") { return true } + if str_eq(w, "has") { return true } + if str_eq(w, "had") { return true } + if str_eq(w, "will") { return true } + if str_eq(w, "shall") { return true } + if str_eq(w, "would") { return true } + if str_eq(w, "should"){ return true } + if str_eq(w, "can") { return true } + if str_eq(w, "could") { return true } + if str_eq(w, "may") { return true } + if str_eq(w, "might") { return true } + if str_eq(w, "must") { return true } + return false +} + +// Subordinating conjunctions that segment a clause (concept-neutral English set). +fn cp_is_subordinator(w: String) -> Bool { + if str_eq(w, "because") { return true } + if str_eq(w, "since") { return true } + if str_eq(w, "although") { return true } + if str_eq(w, "though") { return true } + if str_eq(w, "if") { return true } + if str_eq(w, "when") { return true } + if str_eq(w, "while") { return true } + if str_eq(w, "before") { return true } + if str_eq(w, "after") { return true } + if str_eq(w, "until") { return true } + return false +} + +// ── invertible English verb morphology (the realizer table, run BACKWARD) ────── +// +// cp_irr2(surface) -> [lemma, tense] or empty. Mirrors en_irregular_verb rows in +// morphology.el (same table), inverted, plus "fight" which the acceptance set +// needs. This is the ELP invertibility principle: one table both speaks and +// understands. + +fn cp_irr_row(base: String, three: String, past: String, pp: String, ger: String, surface: String) -> [String] { + let r: [String] = native_list_empty() + if str_eq(surface, past) { + let r = native_list_append(r, base) + let r = native_list_append(r, "past") + return r + } + if str_eq(surface, pp) { + let r = native_list_append(r, base) + let r = native_list_append(r, "past") + return r + } + if str_eq(surface, three) { + let r = native_list_append(r, base) + let r = native_list_append(r, "present") + return r + } + if str_eq(surface, ger) { + let r = native_list_append(r, base) + let r = native_list_append(r, "present") + return r + } + if str_eq(surface, base) { + let r = native_list_append(r, base) + let r = native_list_append(r, "present") + return r + } + return r +} + +fn cp_irr2(surface: String) -> [String] { + let r: [String] = cp_irr_row("be", "is", "was", "been", "being", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("be", "are", "were", "been", "being", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("have", "has", "had", "had", "having", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("do", "does", "did", "done", "doing", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("go", "goes", "went", "gone", "going", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("say", "says", "said", "said", "saying", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("make", "makes", "made", "made", "making", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("know", "knows", "knew", "known", "knowing", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("take", "takes", "took", "taken", "taking", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("see", "sees", "saw", "seen", "seeing", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("come", "comes", "came", "come", "coming", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("think", "thinks", "thought", "thought", "thinking", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("get", "gets", "got", "gotten", "getting", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("give", "gives", "gave", "given", "giving", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("find", "finds", "found", "found", "finding", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("tell", "tells", "told", "told", "telling", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("leave", "leaves", "left", "left", "leaving", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("feel", "feels", "felt", "felt", "feeling", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("bring", "brings", "brought", "brought", "bringing", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("run", "runs", "ran", "run", "running", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("eat", "eats", "ate", "eaten", "eating", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("read", "reads", "read", "read", "reading", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("fight", "fights", "fought", "fought", "fighting", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("write", "writes", "wrote", "written", "writing", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("begin", "begins", "began", "begun", "beginning", surface) + if native_list_len(r) > 0 { return r } + let r = cp_irr_row("become", "becomes", "became", "become", "becoming", surface) + if native_list_len(r) > 0 { return r } + let empty: [String] = native_list_empty() + return empty +} + +// cp_reg_verb(surface) -> [lemma, tense, aspect] by suffix stripping (regular). +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 r = native_list_append(r, lemma) + let r = native_list_append(r, "present") + let r = native_list_append(r, "progressive") + return r + } + } + if str_ends_with(w, "ied") { + let lemma: String = str_slice(w, 0, n - 3) + "y" + let r = native_list_append(r, lemma) + let r = native_list_append(r, "past") + let r = native_list_append(r, "simple") + return r + } + if str_ends_with(w, "ed") { + if n > 2 { + let lemma: String = 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") + return r + } + } + if str_ends_with(w, "ies") { + let lemma: String = str_slice(w, 0, n - 3) + "y" + let r = native_list_append(r, lemma) + let r = native_list_append(r, "present") + let r = native_list_append(r, "simple") + return r + } + if str_ends_with(w, "es") { + if n > 3 { + let lemma: String = str_slice(w, 0, n - 2) + let r = native_list_append(r, lemma) + let r = native_list_append(r, "present") + let r = native_list_append(r, "simple") + return r + } + } + if str_ends_with(w, "s") { + if n > 2 { + let lemma: String = str_slice(w, 0, n - 1) + let r = native_list_append(r, lemma) + let r = native_list_append(r, "present") + let r = native_list_append(r, "simple") + return r + } + } + let r = native_list_append(r, w) + let r = native_list_append(r, "present") + let r = native_list_append(r, "simple") + return r +} + +// ── word-sense disambiguation (deterministic; engram nearest-region at runtime) ─ +// +// The reference (wsd.py) ranks a FIXED sense inventory by embedding cosine to the +// token's syntactic neighbours using the ENGRAM's own geometry — never an +// external generator. In this offline el module we resolve the same fixed +// inventory by deterministic SYNTACTIC POSITION (subject-follows -> verb, etc.); +// cp_wsd_needs_engram() marks the tokens whose final sense the live runtime +// should confirm via the engram's nearest-region ranker. + +fn cp_is_ambiguous(w: String) -> Bool { + if str_eq(w, "flies") { return true } + if str_eq(w, "fly") { return true } + if str_eq(w, "like") { return true } + if str_eq(w, "saw") { return true } + if str_eq(w, "left") { return true } + if str_eq(w, "rose") { return true } + return false +} + +// Is w a plausible finite verb form (used for subject|verb boundary detection)? +fn cp_is_verb_form(w: String) -> Bool { + let irr: [String] = cp_irr2(w) + if native_list_len(irr) > 0 { return true } + if str_eq(w, "flies") { return true } + if str_eq(w, "fly") { return true } + if str_eq(w, "rose") { return true } + if str_eq(w, "left") { return true } + let n: Int = str_len(w) + if str_ends_with(w, "ing") { if n > 4 { return true } } + if str_ends_with(w, "ed") { if n > 2 { return true } } + if str_ends_with(w, "es") { if n > 3 { return true } } + if str_ends_with(w, "s") { if n > 2 { return true } } + return false +} + +// analyze one verb surface -> [lemma, tense, aspect], with WSD for ambiguous forms. +fn cp_analyze_verb(surface: String) -> [String] { + if str_eq(surface, "flies") { + let r: [String] = native_list_empty() + let r = native_list_append(r, "fly") + let r = native_list_append(r, "present") + let r = native_list_append(r, "simple") + return r + } + let irr: [String] = cp_irr2(surface) + if native_list_len(irr) > 0 { + let r: [String] = native_list_empty() + let r = native_list_append(r, native_list_get(irr, 0)) + let r = native_list_append(r, native_list_get(irr, 1)) + let r = native_list_append(r, "simple") + return r + } + return cp_reg_verb(surface) +} + +// ── clause segmentation helpers ─────────────────────────────────────────────── + +// Index of the first subordinating conjunction in [0,n), or n if none. +fn cp_subord_start(toks: [String], n: Int) -> Int { + let i: Int = 1 + while i < n { + if cp_is_subordinator(native_list_get(toks, i)) { + return i + } + let i = i + 1 + } + return n +} + +// Index where the verb cluster begins (end of the subject NP), in [0,end). +fn cp_verb_start(toks: [String], end: Int) -> Int { + if end == 0 { return 0 } + let first: String = native_list_get(toks, 0) + if !str_eq(cp_pron_concept(first), "") { + return 1 + } + let have_head: Bool = false + let i: Int = 0 + while i < end { + let w: String = native_list_get(toks, i) + if i > 0 { + if cp_is_aux(w) { return i } + if cp_is_negation(w) { return i } + if have_head { + if cp_is_verb_form(w) { return i } + } + } + if !cp_is_determiner(w) { + let have_head = true + } + let i = i + 1 + } + return end +} + +// Join tokens [a,b) with spaces. +fn cp_join_range(toks: [String], a: Int, b: Int) -> String { + let parts: [String] = native_list_empty() + let i: Int = a + while i < b { + let parts = native_list_append(parts, native_list_get(toks, i)) + let i = i + 1 + } + return str_join(parts, " ") +} + +// ── the parser ──────────────────────────────────────────────────────────────── + +fn parse_spec_lang(text: String, lang: String) -> [String] { + let toks: [String] = cp_tokenize(text) + let n: Int = native_list_len(toks) + + let intent: String = "assert" + if str_ends_with(text, "?") { let intent = "question" } + + // clause split: main [0, m_end), subordinate [m_end, n) + let m_end: Int = cp_subord_start(toks, n) + + // SACRED polarity: scan the MAIN clause for any negation lexeme. + let polarity: String = "aff" + let neg_word: String = "" + let pi: Int = 0 + while pi < m_end { + let w: String = native_list_get(toks, pi) + if cp_is_negation(w) { + let polarity = "neg" + if cp_is_neg_adverb(w) { let neg_word = w } + } + let pi = pi + 1 + } + + // subject + let vstart: Int = cp_verb_start(toks, m_end) + let agent: String = "" + let first: String = "" + if n > 0 { let first = native_list_get(toks, 0) } + if vstart == 1 { + if !str_eq(cp_pron_concept(first), "") { + let agent = cp_pron_surface(cp_pron_concept(first)) + } else { + let agent = cp_join_range(toks, 0, 1) + } + } else { + let agent = cp_join_range(toks, 0, vstart) + } + + // verb cluster: walk auxiliaries + main verb + let tense: String = "present" + let aspect: String = "simple" + let predicate: String = "" + let do_support: Bool = false + let saw_be: Bool = false + let found_verb: Bool = false + let i: Int = vstart + while i < m_end { + let w: String = native_list_get(toks, i) + if found_verb { + let i = m_end + } else { + if cp_is_negation(w) { + let i = i + 1 + } else { + if cp_is_aux(w) { + if str_eq(w, "did") { let tense = "past"; let do_support = true } + if str_eq(w, "does") { let tense = "present"; let do_support = true } + if str_eq(w, "do") { let tense = "present"; let do_support = true } + if str_eq(w, "will") { let tense = "future" } + if str_eq(w, "shall") { let tense = "future" } + if str_eq(w, "was") { let tense = "past"; let saw_be = true } + if str_eq(w, "were") { let tense = "past"; let saw_be = true } + if str_eq(w, "is") { let saw_be = true } + if str_eq(w, "are") { let saw_be = true } + if str_eq(w, "am") { let saw_be = true } + if str_eq(w, "has") { let aspect = "perfect" } + if str_eq(w, "have") { let aspect = "perfect" } + if str_eq(w, "had") { let aspect = "perfect"; let tense = "past" } + let i = i + 1 + } else { + // main verb token + if do_support { + let predicate = w + // tense already set by the do-auxiliary; verb is the base form + } else { + let va: [String] = cp_analyze_verb(w) + let predicate = native_list_get(va, 0) + let tense = native_list_get(va, 1) + if str_eq(aspect, "simple") { + let aspect = native_list_get(va, 2) + } + } + let found_verb = true + let i = i + 1 + } + } + } + } + // copula: be-auxiliary with no following lexical verb IS the predicate. + if !found_verb { + if saw_be { + let predicate = "be" + let found_verb = true + } + } + + // complements: object NP, to-recipient (iobj), and PP adjunct (location). + // start scanning after the verb we consumed. + let patient: String = "" + let iobj: String = "" + let location: String = "" + // recompute where complements begin: first token after the main verb. + let cstart: Int = vstart + let scanned: Bool = false + let j: Int = vstart + let seen_v: Bool = false + 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) { + let j = j + 1 + } else { + if cp_is_aux(w) { + let j = j + 1 + } else { + let seen_v = true + let j = j + 1 + } + } + } + } + + let k: Int = cstart + while k < m_end { + let w: String = native_list_get(toks, k) + let is_prep: Bool = cp_is_preposition(w) + if str_eq(w, "like") { let is_prep = true } + if is_prep { + let prep: String = w + let a: Int = k + 1 + let b: Int = a + let run3: Bool = true + while run3 { + if b >= m_end { + let run3 = false + } else { + let wb: String = native_list_get(toks, b) + let wb_prep: Bool = cp_is_preposition(wb) + if str_eq(wb, "like") { let wb_prep = true } + if wb_prep { + let run3 = false + } else { + let b = b + 1 + } + } + } + let np: String = cp_join_range(toks, a, b) + if str_eq(prep, "to") { + let iobj = np + } else { + if str_eq(location, "") { + let location = prep + " " + np + } + } + let k = b + } else { + let a: Int = k + let b: Int = a + let run4: Bool = true + while run4 { + if b >= m_end { + let run4 = false + } else { + let wb: String = native_list_get(toks, b) + let wb_prep: Bool = cp_is_preposition(wb) + if str_eq(wb, "like") { let wb_prep = true } + if wb_prep { + let run4 = false + } else { + let b = b + 1 + } + } + } + if str_eq(patient, "") { + let patient = cp_join_range(toks, a, b) + } + let k = b + } + } + + // subordinate clause: conj + (lightweight) predicate recovery. + let subord_conj: String = "" + let subord_pred: String = "" + if m_end < n { + let subord_conj = native_list_get(toks, m_end) + // find the subordinate predicate: first aux/verb after the conjunction. + let s: Int = m_end + 1 + let sfound: Bool = false + while s < n { + if sfound { + let s = n + } else { + let sw: String = native_list_get(toks, s) + if cp_is_aux(sw) { + if str_eq(sw, "was") { let subord_pred = "be"; let sfound = true } + if str_eq(sw, "were") { let subord_pred = "be"; let sfound = true } + if str_eq(sw, "is") { let subord_pred = "be"; let sfound = true } + if str_eq(sw, "are") { let subord_pred = "be"; let sfound = true } + let s = s + 1 + } else { + if cp_is_verb_form(sw) { + let va2: [String] = cp_analyze_verb(sw) + let subord_pred = native_list_get(va2, 0) + let sfound = true + } + let s = s + 1 + } + } + } + } + + // intent refinement: no subject + leading verb => imperative command. + if str_eq(agent, "") { + if found_verb { + if str_eq(intent, "assert") { let intent = "command" } + } + } + + // ── emit the slot map (realizer-compatible + SACRED polarity) ───────────── + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent"); let spec = native_list_append(spec, intent) + let spec = native_list_append(spec, "agent"); let spec = native_list_append(spec, agent) + let spec = native_list_append(spec, "predicate"); let spec = native_list_append(spec, predicate) + let spec = native_list_append(spec, "patient"); let spec = native_list_append(spec, patient) + let spec = native_list_append(spec, "iobj"); let spec = native_list_append(spec, iobj) + let spec = native_list_append(spec, "location"); let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense"); let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect"); let spec = native_list_append(spec, aspect) + let spec = native_list_append(spec, "polarity"); let spec = native_list_append(spec, polarity) + 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, "lang"); let spec = native_list_append(spec, lang) + return spec +} + +// English default entry point. +fn parse_spec(text: String) -> [String] { + return parse_spec_lang(text, "en") +} + +// ── JSON emission (integration contract for generate_lang / build_form_from_json) +fn cp_json_field(key: String, val: String) -> String { + return "\"" + key + "\": \"" + val + "\"" +} + +fn parse_json_lang(text: String, lang: String) -> String { + let spec: [String] = parse_spec_lang(text, lang) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, cp_json_field("intent", slots_get(spec, "intent"))) + let parts = native_list_append(parts, cp_json_field("agent", slots_get(spec, "agent"))) + let parts = native_list_append(parts, cp_json_field("predicate", slots_get(spec, "predicate"))) + let parts = native_list_append(parts, cp_json_field("patient", slots_get(spec, "patient"))) + let parts = native_list_append(parts, cp_json_field("iobj", slots_get(spec, "iobj"))) + let parts = native_list_append(parts, cp_json_field("location", slots_get(spec, "location"))) + let parts = native_list_append(parts, cp_json_field("tense", slots_get(spec, "tense"))) + let parts = native_list_append(parts, cp_json_field("aspect", slots_get(spec, "aspect"))) + let parts = native_list_append(parts, cp_json_field("polarity", slots_get(spec, "polarity"))) + let parts = native_list_append(parts, cp_json_field("neg_word", slots_get(spec, "neg_word"))) + let parts = native_list_append(parts, cp_json_field("lang", lang)) + return "{" + str_join(parts, ", ") + "}" +} + +fn parse_json(text: String) -> String { + return parse_json_lang(text, "en") +} diff --git a/elp/src/comprehend.elh b/elp/src/comprehend.elh new file mode 100644 index 0000000..113d7d2 --- /dev/null +++ b/elp/src/comprehend.elh @@ -0,0 +1,16 @@ +// comprehend.elh — public surface of the ELP comprehension front-end. +// text → meaning-spec (the input half of the ELP; inverse of the realizer). +extern fn parse_spec(text: String) -> [String] +extern fn parse_spec_lang(text: String, lang: String) -> [String] +extern fn parse_json(text: String) -> String +extern fn parse_json_lang(text: String, lang: String) -> String +// Analysis primitives (invertible morphology + deterministic grammar helpers): +extern fn cp_tokenize(text: String) -> [String] +extern fn cp_pron_concept(w: String) -> String +extern fn cp_is_negation(w: String) -> Bool +extern fn cp_is_neg_adverb(w: String) -> Bool +extern fn cp_irr2(surface: String) -> [String] +extern fn cp_reg_verb(w: String) -> [String] +extern fn cp_analyze_verb(surface: String) -> [String] +extern fn cp_verb_start(toks: [String], end: Int) -> Int +extern fn cp_subord_start(toks: [String], n: Int) -> Int diff --git a/elp/src/elp.el b/elp/src/elp.el index 3269061..ce754e7 100644 --- a/elp/src/elp.el +++ b/elp/src/elp.el @@ -63,6 +63,9 @@ import "morphology-cop.el" import "grammar.el" import "realizer.el" import "semantics.el" + +// ── Comprehension front-end (input half: text → meaning-spec) ───────────────── +import "comprehend.el" // // Entry points: // @@ -117,6 +120,9 @@ fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [Strin let location: String = sem_get(semantic_form_json, "location") let tense: String = sem_get(semantic_form_json, "tense") let aspect: String = sem_get(semantic_form_json, "aspect") + let polarity: String = sem_get(semantic_form_json, "polarity") + let neg_word: String = sem_get(semantic_form_json, "neg_word") + let iobj: String = sem_get(semantic_form_json, "iobj") let form: [String] = native_list_empty() let form = native_list_append(form, "intent") @@ -127,12 +133,19 @@ fn build_form_from_json(semantic_form_json: String, lang_code: String) -> [Strin let form = native_list_append(form, predicate) let form = native_list_append(form, "patient") let form = native_list_append(form, patient) + let form = native_list_append(form, "iobj") + let form = native_list_append(form, iobj) let form = native_list_append(form, "location") let form = native_list_append(form, location) let form = native_list_append(form, "tense") let form = native_list_append(form, tense) let form = native_list_append(form, "aspect") let form = native_list_append(form, aspect) + // SACRED: polarity crosses the JSON boundary and is never inferred away. + let form = native_list_append(form, "polarity") + let form = native_list_append(form, polarity) + let form = native_list_append(form, "neg_word") + let form = native_list_append(form, neg_word) let form = native_list_append(form, "lang") let form = native_list_append(form, lang_code) diff --git a/elp/src/morphology.el b/elp/src/morphology.el index 6d7fe00..037aa76 100644 --- a/elp/src/morphology.el +++ b/elp/src/morphology.el @@ -250,6 +250,7 @@ fn en_irregular_verb(base: String) -> [String] { if str_eq(base, "cut") { let r: [String] = ["cut", "cuts", "cut", "cut", "cutting"]; return r } if str_eq(base, "set") { let r: [String] = ["set", "sets", "set", "set", "setting"]; return r } if str_eq(base, "hit") { let r: [String] = ["hit", "hits", "hit", "hit", "hitting"]; return r } + if str_eq(base, "fight") { let r: [String] = ["fight", "fights","fought", "fought", "fighting"]; return r } return empty } diff --git a/elp/src/realizer.el b/elp/src/realizer.el index b43030e..0dc655e 100644 --- a/elp/src/realizer.el +++ b/elp/src/realizer.el @@ -248,6 +248,56 @@ fn add_punct(s: String, intent: String) -> String { return s + "." } +// ── Polarity-aware negation (SACRED field honored on the generation side) ───── +// +// Negation must never be dropped between comprehension and realization. The +// meaning-spec carries an explicit "polarity" field ("aff"|"neg") and optional +// "neg_word" (standalone negative adverb, e.g. "never"). English uses +// do-support ("did not see") or preverbal adverb ("never fought"); copular "be" +// takes post-verbal "not"; other languages get a preverbal negator particle. + +fn realize_negator(code: String) -> String { + if str_eq(code, "es") { return "no" } + if str_eq(code, "pt") { return "não" } + if str_eq(code, "ca") { return "no" } + if str_eq(code, "it") { return "non" } + if str_eq(code, "fr") { return "ne" } + if str_eq(code, "de") { return "nicht" } + if str_eq(code, "ro") { return "nu" } + return "not" +} + +fn realize_assert_neg_en(predicate: String, tense: String, person: String, number: String, agent: String, patient: String, iobj: String, location: String, neg_word: String, profile: [String]) -> String { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, agent) + if !str_eq(neg_word, "") { + // adverbial negation: "I never fought the ocean." + let verb_surf: String = morph_conjugate(predicate, tense, person, number, profile) + let parts = native_list_append(parts, neg_word) + let parts = native_list_append(parts, verb_surf) + } else { + if str_eq(predicate, "be") { + // copular: "she was not a monster" + let be_form: String = morph_conjugate("be", tense, person, number, profile) + let parts = native_list_append(parts, be_form) + let parts = native_list_append(parts, "not") + } else { + // do-support: "she did not see the man" + let do_form: String = morph_conjugate("do", tense, person, number, profile) + let parts = native_list_append(parts, do_form) + let parts = native_list_append(parts, "not") + let parts = native_list_append(parts, predicate) + } + } + if !str_eq(patient, "") { let parts = native_list_append(parts, patient) } + if !str_eq(iobj, "") { + let parts = native_list_append(parts, "to") + let parts = native_list_append(parts, iobj) + } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + return str_join(parts, " ") +} + // ── Main realization entry point ────────────────────────────────────────────── fn realize_lang(form: [String], profile: [String]) -> String { @@ -284,6 +334,36 @@ fn realize_lang(form: [String], profile: [String]) -> String { } // ── Assertion (declarative) ─────────────────────────────────────────────── + let polarity: String = slots_get(form, "polarity") + let neg_word: String = slots_get(form, "neg_word") + let iobj: String = slots_get(form, "iobj") + let code: String = lang_get(profile, "code") + + // Negative polarity: SACRED — never dropped. + if str_eq(polarity, "neg") { + if str_eq(code, "en") { + let sentence: String = realize_assert_neg_en(predicate, tense, person, number, agent, patient, iobj, location, neg_word, profile) + return add_punct(capitalize_first(sentence), "assert") + } + // Generic non-English: affirmative core with a preverbal negator particle. + let neg_particle: String = realize_negator(code) + let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + let vp_str: String = neg_particle + " " + gram_build_vp(verb_surf, aux_surf, profile) + let core: String = gram_order_constituents(agent, vp_str, patient, profile) + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, core) + if !str_eq(iobj, "") { + let parts = native_list_append(parts, "to") + let parts = native_list_append(parts, iobj) + } + if !str_eq(location, "") { let parts = native_list_append(parts, location) } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") + } + + // Affirmative. let vp_pair: [String] = realize_vp_lang(predicate, tense, aspect, person, number, profile) let verb_surf: String = native_list_get(vp_pair, 0) let aux_surf: String = native_list_get(vp_pair, 1) @@ -293,6 +373,10 @@ fn realize_lang(form: [String], profile: [String]) -> String { let parts: [String] = native_list_empty() let parts = native_list_append(parts, core) + if !str_eq(iobj, "") { + let parts = native_list_append(parts, "to") + let parts = native_list_append(parts, iobj) + } if !str_eq(location, "") { let parts = native_list_append(parts, location) } diff --git a/elp/tests/comprehend_gate.el b/elp/tests/comprehend_gate.el new file mode 100644 index 0000000..bdcc30e --- /dev/null +++ b/elp/tests/comprehend_gate.el @@ -0,0 +1,93 @@ +// comprehend_gate.el - the TELEPHONE TEST in native el (acceptance gate). +// +// For each of the 5 acceptance sentences: parse -> spec, realize the spec back +// to English, re-parse the realized surface, and require the SACRED polarity to +// survive the round-trip (and to have been extracted correctly in the first +// place). Mirrors roundtrip.py's GATE, but fully el-native (no LLM, no spaCy). + +fn cp_line(text: String, expected_pol: String) -> String { + let spec: [String] = parse_spec(text) + let pol_in: String = slots_get(spec, "polarity") + let pred: String = slots_get(spec, "predicate") + let surf: String = realize(spec) + let spec2: [String] = parse_spec(surf) + let pol_out: String = slots_get(spec2, "polarity") + let status: String = "LOST" + if str_eq(pol_in, pol_out) { let status = "PRESERVED" } + let okexp: String = "MISMATCH" + if str_eq(pol_in, expected_pol) { let okexp = "ok" } + let out: String = "IN: " + text + "\n" + let out = out + " spec: pol=" + pol_in + " pred=" + pred + let out = out + " agent=" + slots_get(spec, "agent") + let out = out + " pat=" + slots_get(spec, "patient") + let out = out + " iobj=" + slots_get(spec, "iobj") + let out = out + " loc=" + slots_get(spec, "location") + let out = out + " tense=" + slots_get(spec, "tense") + let out = out + " negw=" + slots_get(spec, "neg_word") + let out = out + " subord=" + slots_get(spec, "subord_conj") + "/" + slots_get(spec, "subord_pred") + "\n" + let out = out + " realized: " + surf + "\n" + let out = out + " reparse: pol=" + pol_out + " [" + status + "] expected=" + expected_pol + " (" + okexp + ")\n" + return out +} + +fn cp_preserved(text: String) -> Int { + let spec: [String] = parse_spec(text) + let pol_in: String = slots_get(spec, "polarity") + let surf: String = realize(spec) + let spec2: [String] = parse_spec(surf) + let pol_out: String = slots_get(spec2, "polarity") + if str_eq(pol_in, pol_out) { return 1 } + return 0 +} + +fn cp_correct(text: String, expected_pol: String) -> Int { + let spec: [String] = parse_spec(text) + if str_eq(slots_get(spec, "polarity"), expected_pol) { return 1 } + return 0 +} + +fn run_gate() -> String { + let s1: String = "I never fought the ocean." + let s2: String = "She did not see the man with the telescope." + let s3: String = "The teacher reads the book to the children." + let s4: String = "The stupid boy ate the cat because he was a monster." + let s5: String = "Time flies like an arrow." + + let rep: String = "==== ELP native telephone test (parse -> realize -> re-parse) ====\n" + let rep = rep + cp_line(s1, "neg") + let rep = rep + cp_line(s2, "neg") + let rep = rep + cp_line(s3, "aff") + let rep = rep + cp_line(s4, "aff") + let rep = rep + cp_line(s5, "aff") + + // NOTE: accumulate with Int-var + literal increments — el's overloaded `+` + // mis-compiles chained function-call int operands as string concat. + let pres: Int = 0 + if cp_preserved(s1) == 1 { let pres = pres + 1 } + if cp_preserved(s2) == 1 { let pres = pres + 1 } + if cp_preserved(s3) == 1 { let pres = pres + 1 } + if cp_preserved(s4) == 1 { let pres = pres + 1 } + if cp_preserved(s5) == 1 { let pres = pres + 1 } + let corr: Int = 0 + if cp_correct(s1, "neg") == 1 { let corr = corr + 1 } + if cp_correct(s2, "neg") == 1 { let corr = corr + 1 } + if cp_correct(s3, "aff") == 1 { let corr = corr + 1 } + if cp_correct(s4, "aff") == 1 { let corr = corr + 1 } + if cp_correct(s5, "aff") == 1 { let corr = corr + 1 } + + let rep = rep + "-----------------------------------------------------------------\n" + let rep = rep + "polarity PRESERVED through round-trip: " + int_to_str(pres) + "/5\n" + let rep = rep + "polarity EXTRACTED correctly: " + int_to_str(corr) + "/5\n" + if pres == 5 { + if corr == 5 { + let rep = rep + "GATE: PASS\n" + } else { + let rep = rep + "GATE: FAIL (extraction)\n" + } + } else { + let rep = rep + "GATE: FAIL (round-trip)\n" + } + return rep +} + +println(run_gate())