From 1432c56cf7769b7f4110219e5bc314a16c63db4e Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sat, 2 May 2026 14:16:23 -0500 Subject: [PATCH] Add native El NLG system: morphology, vocabulary, grammar, realizer Implements a complete natural language generation stack in El: - morphology.el: English pluralization, verb conjugation (40+ irregulars), determiner agreement - vocabulary.el: inline seed lexicon (~100 entries: pronouns, nouns, verbs, adjectives, etc.) - grammar.el: CFG rules (S/NP/VP/PP), slot-map driven tree generator, s-expression renderer - realizer.el: semantic form -> English text with tense/aspect/agreement, do-support for questions - nlg.el: JSON-driven public API tying all modules together - tests/run.sh: acceptance corpus runner (6 tests, all passing) --- elp/manifest.el | 9 + elp/src/grammar.el | 495 +++++++++++++++++++++ elp/src/morphology.el | 566 +++++++++++++++++++++++++ elp/src/nlg.el | 58 +++ elp/src/realizer.el | 261 ++++++++++++ elp/src/vocabulary.el | 328 ++++++++++++++ elp/tests/examples/basic-sentence.el | 24 ++ elp/tests/examples/command.el | 24 ++ elp/tests/examples/past-tense.el | 24 ++ elp/tests/examples/plural-noun.el | 16 + elp/tests/examples/question.el | 24 ++ elp/tests/examples/verb-conjugation.el | 15 + elp/tests/run.sh | 112 +++++ 13 files changed, 1956 insertions(+) create mode 100644 elp/manifest.el create mode 100644 elp/src/grammar.el create mode 100644 elp/src/morphology.el create mode 100644 elp/src/nlg.el create mode 100644 elp/src/realizer.el create mode 100644 elp/src/vocabulary.el create mode 100644 elp/tests/examples/basic-sentence.el create mode 100644 elp/tests/examples/command.el create mode 100644 elp/tests/examples/past-tense.el create mode 100644 elp/tests/examples/plural-noun.el create mode 100644 elp/tests/examples/question.el create mode 100644 elp/tests/examples/verb-conjugation.el create mode 100755 elp/tests/run.sh diff --git a/elp/manifest.el b/elp/manifest.el new file mode 100644 index 0000000..e2469e2 --- /dev/null +++ b/elp/manifest.el @@ -0,0 +1,9 @@ +package "nlg" { + version "0.1.0" + description "Native El natural language generation - vocabulary, grammar, realizer, morphology" + edition "2026" +} + +build { + entry "src/nlg.el" +} diff --git a/elp/src/grammar.el b/elp/src/grammar.el new file mode 100644 index 0000000..708a594 --- /dev/null +++ b/elp/src/grammar.el @@ -0,0 +1,495 @@ +// grammar.el - Context-free grammar for English. +// +// Grammar rules are stored as lists: [id, lhs, rhs_part0, rhs_part1, ...] +// Tree nodes are stored as lists: [label, word, child0, child1, ...] +// where child slots are also lists (nested tree nodes). +// +// This module provides: +// - A catalog of English grammar rules (S, NP, VP, PP) +// - A generator that fills a rule skeleton with semantic slots +// +// Slots are passed as a flat string map encoded as a list: +// ["key1", "val1", "key2", "val2", ...] +// +// Depends on: nothing (standalone) + +// ── Slot map helpers ────────────────────────────────────────────────────────── +// Slot maps are [String] lists: [key, val, key, val, ...] + +fn slots_get(slots: [String], key: String) -> String { + let n: Int = native_list_len(slots) + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(slots, i) + if str_eq(k, key) { + return native_list_get(slots, i + 1) + } + let i = i + 2 + } + return "" +} + +fn slots_set(slots: [String], key: String, val: String) -> [String] { + let n: Int = native_list_len(slots) + let result: [String] = native_list_empty() + let found: Bool = false + let i: Int = 0 + while i < n - 1 { + let k: String = native_list_get(slots, i) + let v: String = native_list_get(slots, i + 1) + if str_eq(k, key) { + let result = native_list_append(result, k) + let result = native_list_append(result, val) + let found = true + } else { + let result = native_list_append(result, k) + let result = native_list_append(result, v) + } + let i = i + 2 + } + if !found { + let result = native_list_append(result, key) + let result = native_list_append(result, val) + } + return result +} + +fn make_slots(k0: String, v0: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, k0) + let r = native_list_append(r, v0) + return r +} + +fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String] { + let r: [String] = make_slots(k0, v0) + let r = native_list_append(r, k1) + let r = native_list_append(r, v1) + return r +} + +fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String] { + let r: [String] = make_slots2(k0, v0, k1, v1) + let r = native_list_append(r, k2) + let r = native_list_append(r, v2) + return r +} + +fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String] { + let r: [String] = make_slots3(k0, v0, k1, v1, k2, v2) + let r = native_list_append(r, k3) + let r = native_list_append(r, v3) + return r +} + +fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String] { + let r: [String] = make_slots4(k0, v0, k1, v1, k2, v2, k3, v3) + let r = native_list_append(r, k4) + let r = native_list_append(r, v4) + return r +} + +// ── Grammar rule catalog ────────────────────────────────────────────────────── +// +// Rules: +// S-DECL S -> NP VP declarative sentence +// S-QUEST S -> Aux NP VP yes/no question +// S-IMP S -> VP imperative +// NP-DET-N NP -> Det N the cat +// NP-DET-ADJ-N NP -> Det Adj N the big cat +// NP-PRON NP -> Pron she/he/they +// NP-N NP -> N proper noun / bare noun +// VP-V VP -> V intransitive +// VP-V-NP VP -> V NP transitive +// VP-V-PP VP -> V PP locative +// VP-V-NP-PP VP -> V NP PP ditransitive+pp +// VP-AUX-V VP -> Aux V modal +// VP-AUX-V-NP VP -> Aux V NP modal transitive +// PP-P-NP PP -> P NP prepositional phrase + +fn rule_id(rule: [String]) -> String { + return native_list_get(rule, 0) +} + +fn rule_lhs(rule: [String]) -> String { + return native_list_get(rule, 1) +} + +fn rule_rhs_len(rule: [String]) -> Int { + let n: Int = native_list_len(rule) + return n - 2 +} + +fn rule_rhs(rule: [String], idx: Int) -> String { + return native_list_get(rule, idx + 2) +} + +fn make_rule(id: String, lhs: String, r0: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, id) + let r = native_list_append(r, lhs) + let r = native_list_append(r, r0) + return r +} + +fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String] { + let r: [String] = make_rule(id, lhs, r0) + let r = native_list_append(r, r1) + return r +} + +fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String] { + let r: [String] = make_rule2(id, lhs, r0, r1) + let r = native_list_append(r, r2) + return r +} + +fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String] { + let r: [String] = make_rule3(id, lhs, r0, r1, r2) + let r = native_list_append(r, r3) + return r +} + +fn build_rules() -> [[String]] { + let rules: [[String]] = native_list_empty() + + // Sentence rules + let rules = native_list_append(rules, make_rule2("S-DECL", "S", "NP", "VP")) + let rules = native_list_append(rules, make_rule3("S-QUEST", "S", "Aux", "NP", "VP")) + let rules = native_list_append(rules, make_rule("S-IMP", "S", "VP")) + + // NP rules + let rules = native_list_append(rules, make_rule2("NP-DET-N", "NP", "Det", "N")) + let rules = native_list_append(rules, make_rule3("NP-DET-ADJ-N","NP","Det", "Adj", "N")) + let rules = native_list_append(rules, make_rule("NP-PRON", "NP", "Pron")) + let rules = native_list_append(rules, make_rule("NP-N", "NP", "N")) + + // VP rules + let rules = native_list_append(rules, make_rule("VP-V", "VP", "V")) + let rules = native_list_append(rules, make_rule2("VP-V-NP", "VP", "V", "NP")) + let rules = native_list_append(rules, make_rule2("VP-V-PP", "VP", "V", "PP")) + let rules = native_list_append(rules, make_rule3("VP-V-NP-PP", "VP", "V", "NP", "PP")) + let rules = native_list_append(rules, make_rule2("VP-AUX-V", "VP", "Aux", "V")) + let rules = native_list_append(rules, make_rule3("VP-AUX-V-NP","VP", "Aux", "V", "NP")) + + // PP rules + let rules = native_list_append(rules, make_rule2("PP-P-NP", "PP", "P", "NP")) + + return rules +} + +fn get_rules() -> [[String]] { + return build_rules() +} + +fn find_rule(rule_id_str: String) -> [String] { + let rules: [[String]] = get_rules() + let n: Int = native_list_len(rules) + let i: Int = 0 + while i < n { + let rule: [String] = native_list_get(rules, i) + let id: String = native_list_get(rule, 0) + if str_eq(id, rule_id_str) { + return rule + } + let i = i + 1 + } + let empty: [String] = native_list_empty() + return empty +} + +// ── Tree node construction ──────────────────────────────────────────────────── +// A tree node is a [String]: [label, word, num_children, c0_size, c0..., c1_size, c1..., ...] +// Since El lists only hold one element type, we serialize tree nodes as +// flattened string lists using a simple s-expression encoding. +// +// Format: "(LABEL WORD CHILD1 CHILD2 ...)" +// Leaf node: "(LABEL WORD)" +// Non-terminal: "(LABEL _ CHILD1 CHILD2)" + +fn make_leaf(label: String, word: String) -> String { + return "(" + label + " " + word + ")" +} + +fn make_node1(label: String, child0: String) -> String { + return "(" + label + " _ " + child0 + ")" +} + +fn make_node2(label: String, child0: String, child1: String) -> String { + return "(" + label + " _ " + child0 + " " + child1 + ")" +} + +fn make_node3(label: String, child0: String, child1: String, child2: String) -> String { + return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + ")" +} + +fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String { + return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + " " + child3 + ")" +} + +// ── Tree rendering: extract the terminal words in order ─────────────────────── +// +// Walk the s-expression and collect all leaf words. + +fn nlg_is_ws(c: String) -> Bool { + if str_eq(c, " ") { return true } + if str_eq(c, "\t") { return true } + if str_eq(c, "\n") { return true } + return false +} + +// Scan forward past whitespace; return new position. +fn skip_ws(s: String, pos: Int) -> Int { + let n: Int = str_len(s) + let i: Int = pos + let running: Bool = true + while running { + if i >= n { + let running = false + } else { + let c: String = str_slice(s, i, i + 1) + if nlg_is_ws(c) { + let i = i + 1 + } else { + let running = false + } + } + } + return i +} + +// Scan a token (non-whitespace, non-paren run); return [token_string, end_pos]. +fn scan_token(s: String, start: Int) -> [String] { + let n: Int = str_len(s) + let i: Int = start + let running: Bool = true + while running { + if i >= n { + let running = false + } else { + let c: String = str_slice(s, i, i + 1) + if nlg_is_ws(c) { + let running = false + } else { + if str_eq(c, "(") { + let running = false + } else { + if str_eq(c, ")") { + let running = false + } else { + let i = i + 1 + } + } + } + } + } + let tok: String = str_slice(s, start, i) + let result: [String] = native_list_empty() + let result = native_list_append(result, tok) + let result = native_list_append(result, int_to_str(i)) + return result +} + +// Collect terminal words from a tree s-expression. +// Words are the second token in each "(LABEL WORD)" pair where WORD != "_". +// render a tree to a flat string by collecting leaf words. +// We walk the s-expression character by character. + +fn render_tree(tree: String) -> String { + let words: [String] = native_list_empty() + let n: Int = str_len(tree) + let i: Int = 0 + // Track depth: after opening paren, the first non-_ token at depth 1 + // that is followed by a closing paren (or more tokens) is a leaf word. + // Strategy: extract all tokens, skip labels (first after '(') and '_'. + // All other tokens that aren't '(' or ')' are leaf words. + let prev_was_open: Bool = false + let is_first_after_open: Bool = false + while i < n { + let c: String = str_slice(tree, i, i + 1) + if str_eq(c, "(") { + let prev_was_open = true + let i = i + 1 + } else { + if str_eq(c, ")") { + let prev_was_open = false + let i = i + 1 + } else { + if nlg_is_ws(c) { + let i = i + 1 + } else { + // Start of a token + let tok_info: [String] = scan_token(tree, i) + let tok: String = native_list_get(tok_info, 0) + let new_i: Int = str_to_int(native_list_get(tok_info, 1)) + let i = new_i + // If this is the first token after '(' it is a label - skip + if prev_was_open { + let prev_was_open = false + // skip label + } else { + // It's a word or '_' placeholder + if !str_eq(tok, "_") { + let words = native_list_append(words, tok) + } + } + } + } + } + } + return str_join(words, " ") +} + +// ── Tree generator ──────────────────────────────────────────────────────────── +// +// generate_tree(rule_id, slots) -> tree s-expression string +// slots: a [String] list of [key, val, key, val, ...] pairs +// +// Known slot keys: +// "agent" - NP subject (pronoun or noun phrase string) +// "predicate" - verb base form +// "patient" - NP object (noun phrase string, optional) +// "location" - PP location (e.g. "in the park") +// "tense" - "present" | "past" | "future" +// "aspect" - "simple" | "progressive" | "perfect" +// "det" - determiner for subject NP +// "aux" - auxiliary for questions +// "verb_surf" - pre-conjugated verb surface form +// "aux_surf" - pre-conjugated auxiliary surface form + +fn generate_tree(rule_id_str: String, slots: [String]) -> String { + let rule: [String] = find_rule(rule_id_str) + let n: Int = native_list_len(rule) + if n == 0 { + return make_leaf("ERR", "unknown-rule") + } + + let lhs: String = native_list_get(rule, 1) + let rhs_n: Int = n - 2 + + // ── S rules ─────────────────────────────────────────────────────────────── + if str_eq(rule_id_str, "S-DECL") { + let agent: String = slots_get(slots, "agent") + let np_tree: String = build_np(agent, slots) + let vp_tree: String = build_vp_from_slots(slots) + return make_node2("S", np_tree, vp_tree) + } + + if str_eq(rule_id_str, "S-QUEST") { + let agent: String = slots_get(slots, "agent") + let np_tree: String = build_np(agent, slots) + let vp_tree: String = build_vp_body(slots) + let aux_surf: String = slots_get(slots, "aux_surf") + return make_node3("S", make_leaf("Aux", aux_surf), np_tree, vp_tree) + } + + if str_eq(rule_id_str, "S-IMP") { + let vp_tree: String = build_vp_from_slots(slots) + return make_node1("S", vp_tree) + } + + return make_leaf(lhs, "?") +} + +// Build an NP tree from a referent string. +// If the referent is a pronoun (I, you, he, she, it, we, they, me, him, her, us, them), +// use NP-PRON. If it looks like "the X" or "a X", parse accordingly. +// Otherwise treat as a proper noun. + +fn is_pronoun(word: String) -> Bool { + if str_eq(word, "I") { return true } + if str_eq(word, "you") { return true } + if str_eq(word, "he") { return true } + if str_eq(word, "she") { return true } + if str_eq(word, "it") { return true } + if str_eq(word, "we") { return true } + if str_eq(word, "they") { return true } + if str_eq(word, "me") { return true } + if str_eq(word, "him") { return true } + if str_eq(word, "her") { return true } + if str_eq(word, "us") { return true } + if str_eq(word, "them") { return true } + return false +} + +fn build_np(referent: String, slots: [String]) -> String { + if is_pronoun(referent) { + return make_node1("NP", make_leaf("Pron", referent)) + } + // Try to parse "DET NOUN" or "DET ADJ NOUN" from the referent string + let parts: [String] = str_split(referent, " ") + let np: Int = native_list_len(parts) + if np == 1 { + // Single word - proper noun or bare noun + return make_node1("NP", make_leaf("N", referent)) + } + if np == 2 { + // DET NOUN + let det: String = native_list_get(parts, 0) + let noun: String = native_list_get(parts, 1) + return make_node2("NP", make_leaf("Det", det), make_leaf("N", noun)) + } + if np == 3 { + // DET ADJ NOUN + let det: String = native_list_get(parts, 0) + let adj: String = native_list_get(parts, 1) + let noun: String = native_list_get(parts, 2) + return make_node3("NP", make_leaf("Det", det), make_leaf("Adj", adj), make_leaf("N", noun)) + } + // Fallback: treat the whole thing as a name + return make_node1("NP", make_leaf("N", referent)) +} + +fn build_pp(loc: String) -> String { + // loc is expected as "PREP NP" e.g. "in the park" + let parts: [String] = str_split(loc, " ") + let n: Int = native_list_len(parts) + if n < 2 { + return make_leaf("PP", loc) + } + let prep: String = native_list_get(parts, 0) + // Rest is the NP + let np_parts: [String] = native_list_empty() + let i: Int = 1 + while i < n { + let np_parts = native_list_append(np_parts, native_list_get(parts, i)) + let i = i + 1 + } + let np_str: String = str_join(np_parts, " ") + let np_tree: String = build_np(np_str, native_list_empty()) + return make_node2("PP", make_leaf("P", prep), np_tree) +} + +fn build_vp_body(slots: [String]) -> String { + let verb_surf: String = slots_get(slots, "verb_surf") + let patient: String = slots_get(slots, "patient") + let loc: String = slots_get(slots, "location") + if !str_eq(patient, "") { + let obj_np: String = build_np(patient, slots) + if !str_eq(loc, "") { + let pp: String = build_pp(loc) + return make_node3("VP", make_leaf("V", verb_surf), obj_np, pp) + } + return make_node2("VP", make_leaf("V", verb_surf), obj_np) + } + if !str_eq(loc, "") { + let pp: String = build_pp(loc) + return make_node2("VP", make_leaf("V", verb_surf), pp) + } + return make_node1("VP", make_leaf("V", verb_surf)) +} + +fn build_vp_from_slots(slots: [String]) -> String { + let aux_surf: String = slots_get(slots, "aux_surf") + if !str_eq(aux_surf, "") { + let verb_surf: String = slots_get(slots, "verb_surf") + let patient: String = slots_get(slots, "patient") + let loc: String = slots_get(slots, "location") + if !str_eq(patient, "") { + let obj_np: String = build_np(patient, slots) + return make_node3("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf), obj_np) + } + return make_node2("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf)) + } + return build_vp_body(slots) +} diff --git a/elp/src/morphology.el b/elp/src/morphology.el new file mode 100644 index 0000000..493776f --- /dev/null +++ b/elp/src/morphology.el @@ -0,0 +1,566 @@ +// morphology.el - English morphology: pluralization, verb conjugation, agreement. +// +// Handles regular rules and the ~40 most common irregular forms. +// Standalone: no dependencies on other NLG modules. + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +fn str_ends(s: String, suf: String) -> Bool { + return str_ends_with(s, suf) +} + +fn str_last_char(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return "" + } + return str_slice(s, n - 1, n) +} + +fn str_last2(s: String) -> String { + let n: Int = str_len(s) + if n < 2 { + return s + } + return str_slice(s, n - 2, n) +} + +fn str_last3(s: String) -> String { + let n: Int = str_len(s) + if n < 3 { + return s + } + return str_slice(s, n - 3, n) +} + +fn 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 is_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 +} + +// ── Irregular noun plurals ──────────────────────────────────────────────────── + +fn irregular_plural(word: String) -> String { + // Truly irregular + if str_eq(word, "child") { return "children" } + if str_eq(word, "man") { return "men" } + if str_eq(word, "woman") { return "women" } + if str_eq(word, "tooth") { return "teeth" } + if str_eq(word, "foot") { return "feet" } + if str_eq(word, "goose") { return "geese" } + if str_eq(word, "mouse") { return "mice" } + if str_eq(word, "louse") { return "lice" } + if str_eq(word, "ox") { return "oxen" } + if str_eq(word, "person") { return "people" } + if str_eq(word, "leaf") { return "leaves" } + if str_eq(word, "loaf") { return "loaves" } + if str_eq(word, "wolf") { return "wolves" } + if str_eq(word, "life") { return "lives" } + if str_eq(word, "knife") { return "knives" } + if str_eq(word, "wife") { return "wives" } + if str_eq(word, "half") { return "halves" } + if str_eq(word, "self") { return "selves" } + if str_eq(word, "elf") { return "elves" } + if str_eq(word, "shelf") { return "shelves" } + // Zero-plural (same singular and plural) + if str_eq(word, "fish") { return "fish" } + if str_eq(word, "sheep") { return "sheep" } + if str_eq(word, "deer") { return "deer" } + if str_eq(word, "moose") { return "moose" } + if str_eq(word, "series") { return "series" } + if str_eq(word, "species") { return "species" } + return "" +} + +fn irregular_singular(word: String) -> String { + if str_eq(word, "children") { return "child" } + if str_eq(word, "men") { return "man" } + if str_eq(word, "women") { return "woman" } + if str_eq(word, "teeth") { return "tooth" } + if str_eq(word, "feet") { return "foot" } + if str_eq(word, "geese") { return "goose" } + if str_eq(word, "mice") { return "mouse" } + if str_eq(word, "lice") { return "louse" } + if str_eq(word, "oxen") { return "ox" } + if str_eq(word, "people") { return "person" } + if str_eq(word, "leaves") { return "leaf" } + if str_eq(word, "wolves") { return "wolf" } + if str_eq(word, "lives") { return "life" } + if str_eq(word, "knives") { return "knife" } + if str_eq(word, "wives") { return "wife" } + if str_eq(word, "halves") { return "half" } + if str_eq(word, "selves") { return "self" } + if str_eq(word, "elves") { return "elf" } + if str_eq(word, "shelves") { return "shelf" } + if str_eq(word, "fish") { return "fish" } + if str_eq(word, "sheep") { return "sheep" } + if str_eq(word, "deer") { return "deer" } + if str_eq(word, "moose") { return "moose" } + if str_eq(word, "series") { return "series" } + if str_eq(word, "species") { return "species" } + return "" +} + +// ── Noun pluralization ──────────────────────────────────────────────────────── + +fn pluralize(singular: String) -> String { + // Check irregulars first + let irreg: String = irregular_plural(singular) + if !str_eq(irreg, "") { + return irreg + } + + // -s, -x, -z, -ch, -sh -> +es + if str_ends(singular, "s") { return singular + "es" } + if str_ends(singular, "x") { return singular + "es" } + if str_ends(singular, "z") { return singular + "es" } + if str_ends(singular, "ch") { return singular + "es" } + if str_ends(singular, "sh") { return singular + "es" } + + // consonant + y -> -y +ies + let last: String = str_last_char(singular) + if str_eq(last, "y") { + let prev: String = str_drop_last(singular, 1) + let prev_last: String = str_last_char(prev) + if !is_vowel(prev_last) { + return prev + "ies" + } + } + + // -fe -> -ves + if str_ends(singular, "fe") { + return str_drop_last(singular, 2) + "ves" + } + + // -f -> -ves (for common words; not universal) + // skip - too many exceptions (roof, belief, cliff) + + // Default: +s + return singular + "s" +} + +fn singularize(plural: String) -> String { + // Check irregulars first + let irreg: String = irregular_singular(plural) + if !str_eq(irreg, "") { + return irreg + } + + // -ies -> -y + if str_ends(plural, "ies") { + return str_drop_last(plural, 3) + "y" + } + + // -ves -> -f or -fe (best effort: try -fe first, else -f) + if str_ends(plural, "ves") { + // A few specific ones we know; otherwise default to -f + let stem: String = str_drop_last(plural, 3) + let last_stem: String = str_last_char(stem) + if str_eq(last_stem, "i") { + // lives -> life, knives -> knife, wives -> wife + return stem + "fe" + } + return stem + "f" + } + + // -es (for -s, -x, -z, -ch, -sh endings) + if str_ends(plural, "ches") { return str_drop_last(plural, 2) } + if str_ends(plural, "shes") { return str_drop_last(plural, 2) } + if str_ends(plural, "xes") { return str_drop_last(plural, 2) } + if str_ends(plural, "zes") { return str_drop_last(plural, 2) } + if str_ends(plural, "ses") { return str_drop_last(plural, 2) } + + // Default: -s + if str_ends(plural, "s") { + return str_drop_last(plural, 1) + } + + return plural +} + +// ── Irregular verb forms ────────────────────────────────────────────────────── +// Returns a list: [base, 3sg-present, past, past-participle, gerund] + +fn irregular_verb(base: String) -> [String] { + let empty: [String] = [] + if str_eq(base, "be") { + let r: [String] = ["be", "is", "was", "been", "being"] + return r + } + if str_eq(base, "have") { + let r: [String] = ["have", "has", "had", "had", "having"] + return r + } + if str_eq(base, "do") { + let r: [String] = ["do", "does", "did", "done", "doing"] + return r + } + if str_eq(base, "go") { + let r: [String] = ["go", "goes", "went", "gone", "going"] + return r + } + if str_eq(base, "say") { + let r: [String] = ["say", "says", "said", "said", "saying"] + return r + } + if str_eq(base, "make") { + let r: [String] = ["make", "makes", "made", "made", "making"] + return r + } + if str_eq(base, "know") { + let r: [String] = ["know", "knows", "knew", "known", "knowing"] + return r + } + if str_eq(base, "take") { + let r: [String] = ["take", "takes", "took", "taken", "taking"] + return r + } + if str_eq(base, "see") { + let r: [String] = ["see", "sees", "saw", "seen", "seeing"] + return r + } + if str_eq(base, "come") { + let r: [String] = ["come", "comes", "came", "come", "coming"] + return r + } + if str_eq(base, "think") { + let r: [String] = ["think", "thinks", "thought", "thought", "thinking"] + return r + } + if str_eq(base, "get") { + let r: [String] = ["get", "gets", "got", "gotten", "getting"] + return r + } + if str_eq(base, "give") { + let r: [String] = ["give", "gives", "gave", "given", "giving"] + return r + } + if str_eq(base, "find") { + let r: [String] = ["find", "finds", "found", "found", "finding"] + return r + } + if str_eq(base, "tell") { + let r: [String] = ["tell", "tells", "told", "told", "telling"] + return r + } + if str_eq(base, "become") { + let r: [String] = ["become", "becomes", "became", "become", "becoming"] + return r + } + if str_eq(base, "leave") { + let r: [String] = ["leave", "leaves", "left", "left", "leaving"] + return r + } + if str_eq(base, "feel") { + let r: [String] = ["feel", "feels", "felt", "felt", "feeling"] + return r + } + if str_eq(base, "put") { + let r: [String] = ["put", "puts", "put", "put", "putting"] + return r + } + if str_eq(base, "bring") { + let r: [String] = ["bring", "brings", "brought", "brought", "bringing"] + return r + } + if str_eq(base, "begin") { + let r: [String] = ["begin", "begins", "began", "begun", "beginning"] + return r + } + if str_eq(base, "keep") { + let r: [String] = ["keep", "keeps", "kept", "kept", "keeping"] + return r + } + if str_eq(base, "hold") { + let r: [String] = ["hold", "holds", "held", "held", "holding"] + return r + } + if str_eq(base, "write") { + let r: [String] = ["write", "writes", "wrote", "written", "writing"] + return r + } + if str_eq(base, "stand") { + let r: [String] = ["stand", "stands", "stood", "stood", "standing"] + return r + } + if str_eq(base, "hear") { + let r: [String] = ["hear", "hears", "heard", "heard", "hearing"] + return r + } + if str_eq(base, "let") { + let r: [String] = ["let", "lets", "let", "let", "letting"] + return r + } + if str_eq(base, "run") { + let r: [String] = ["run", "runs", "ran", "run", "running"] + return r + } + if str_eq(base, "meet") { + let r: [String] = ["meet", "meets", "met", "met", "meeting"] + return r + } + if str_eq(base, "sit") { + let r: [String] = ["sit", "sits", "sat", "sat", "sitting"] + return r + } + if str_eq(base, "send") { + let r: [String] = ["send", "sends", "sent", "sent", "sending"] + return r + } + if str_eq(base, "speak") { + let r: [String] = ["speak", "speaks", "spoke", "spoken", "speaking"] + return r + } + if str_eq(base, "buy") { + let r: [String] = ["buy", "buys", "bought", "bought", "buying"] + return r + } + if str_eq(base, "pay") { + let r: [String] = ["pay", "pays", "paid", "paid", "paying"] + return r + } + if str_eq(base, "read") { + let r: [String] = ["read", "reads", "read", "read", "reading"] + return r + } + if str_eq(base, "win") { + let r: [String] = ["win", "wins", "won", "won", "winning"] + return r + } + if str_eq(base, "eat") { + let r: [String] = ["eat", "eats", "ate", "eaten", "eating"] + return r + } + if str_eq(base, "fall") { + let r: [String] = ["fall", "falls", "fell", "fallen", "falling"] + return r + } + if str_eq(base, "sleep") { + let r: [String] = ["sleep", "sleeps", "slept", "slept", "sleeping"] + return r + } + if str_eq(base, "drive") { + let r: [String] = ["drive", "drives", "drove", "driven", "driving"] + return r + } + if str_eq(base, "build") { + let r: [String] = ["build", "builds", "built", "built", "building"] + return r + } + 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 + } + return empty +} + +// ── Verb 3sg present form ───────────────────────────────────────────────────── + +fn verb_3sg(base: String) -> String { + // -s, -x, -z, -ch, -sh -> +es + if str_ends(base, "s") { return base + "es" } + if str_ends(base, "x") { return base + "es" } + if str_ends(base, "z") { return base + "es" } + if str_ends(base, "ch") { return base + "es" } + if str_ends(base, "sh") { return base + "es" } + + // consonant + y -> -y +ies + let last: String = str_last_char(base) + if str_eq(last, "y") { + let prev: String = str_drop_last(base, 1) + let prev_last: String = str_last_char(prev) + if !is_vowel(prev_last) { + return prev + "ies" + } + } + + return base + "s" +} + +// ── Verb past tense form ────────────────────────────────────────────────────── + +fn should_double_final(base: String) -> Bool { + // CVC pattern: single syllable ending in consonant-vowel-consonant + // and the final consonant is not w, x, y + let n: Int = str_len(base) + if n < 3 { + return false + } + let c3: String = str_slice(base, n - 3, n - 2) + let c2: String = str_slice(base, n - 2, n - 1) + let c1: String = str_slice(base, n - 1, n) + if !is_vowel(c3) { + if is_vowel(c2) { + if !is_vowel(c1) { + if !str_eq(c1, "w") { + if !str_eq(c1, "x") { + if !str_eq(c1, "y") { + return true + } + } + } + } + } + } + return false +} + +fn verb_past(base: String) -> String { + // Ends in -e: just add -d + if str_ends(base, "e") { + return base + "d" + } + + // consonant + y -> -y +ied + let last: String = str_last_char(base) + if str_eq(last, "y") { + let prev: String = str_drop_last(base, 1) + let prev_last: String = str_last_char(prev) + if !is_vowel(prev_last) { + return prev + "ied" + } + } + + // Double final consonant + if should_double_final(base) { + return base + last + "ed" + } + + return base + "ed" +} + +// ── Verb gerund form ────────────────────────────────────────────────────────── + +fn verb_gerund(base: String) -> String { + // Ends in -ie: drop -ie, add -ying + if str_ends(base, "ie") { + return str_drop_last(base, 2) + "ying" + } + + // Ends in -e (not -ee): drop -e, add -ing + if str_ends(base, "e") { + if !str_ends(base, "ee") { + return str_drop_last(base, 1) + "ing" + } + } + + // Double final consonant (same CVC rule as past) + let last: String = str_last_char(base) + if should_double_final(base) { + return base + last + "ing" + } + + return base + "ing" +} + +// ── Main verb conjugation ───────────────────────────────────────────────────── +// +// tense: "present" | "past" | "future" | "perfect" | "progressive" +// person: "first" | "second" | "third" +// number: "singular" | "plural" + +fn verb_form(base: String, tense: String, person: String, number: String) -> String { + let irreg: [String] = irregular_verb(base) + let is_irreg: Bool = false + if native_list_len(irreg) > 0 { + let is_irreg = true + } + + // ── "be" special-cased for all persons ─────────────────────────────────── + if str_eq(base, "be") { + if str_eq(tense, "present") { + if str_eq(number, "plural") { return "are" } + if str_eq(person, "first") { return "am" } + if str_eq(person, "second") { return "are" } + return "is" + } + if str_eq(tense, "past") { + if str_eq(number, "plural") { return "were" } + if str_eq(person, "second") { return "were" } + return "was" + } + if str_eq(tense, "future") { return "will be" } + if str_eq(tense, "perfect") { return "been" } + if str_eq(tense, "progressive") { return "being" } + return "be" + } + + // ── present ────────────────────────────────────────────────────────────── + if str_eq(tense, "present") { + if str_eq(person, "third") { + if str_eq(number, "singular") { + if is_irreg { + return native_list_get(irreg, 1) + } + return verb_3sg(base) + } + } + return base + } + + // ── past ───────────────────────────────────────────────────────────────── + if str_eq(tense, "past") { + if is_irreg { + return native_list_get(irreg, 2) + } + return verb_past(base) + } + + // ── future ─────────────────────────────────────────────────────────────── + if str_eq(tense, "future") { + return "will " + base + } + + // ── perfect (past participle) ───────────────────────────────────────────── + if str_eq(tense, "perfect") { + if is_irreg { + return native_list_get(irreg, 3) + } + return verb_past(base) + } + + // ── progressive (gerund/present participle) ─────────────────────────────── + if str_eq(tense, "progressive") { + if is_irreg { + return native_list_get(irreg, 4) + } + return verb_gerund(base) + } + + return base +} + +// ── Determiner agreement ────────────────────────────────────────────────────── +// "a" -> "an" before vowel sounds. + +fn agree_determiner(det: String, noun: String) -> String { + if str_eq(det, "a") { + let first: String = str_slice(noun, 0, 1) + let fl: String = str_to_lower(first) + if is_vowel(fl) { + return "an" + } + return "a" + } + return det +} diff --git a/elp/src/nlg.el b/elp/src/nlg.el new file mode 100644 index 0000000..77c543c --- /dev/null +++ b/elp/src/nlg.el @@ -0,0 +1,58 @@ +// nlg.el - Public NLG API. +// +// Ties together vocabulary, morphology, grammar, and realizer into a single +// entry point. Callers use `generate(semantic_form_json)` to produce English. +// +// SemanticForm JSON fields (all strings): +// intent - "assert" | "question" | "command" +// agent - subject (pronoun or noun phrase, optional for commands) +// predicate - verb base form +// patient - object noun phrase (optional) +// location - prepositional phrase e.g. "in the park" (optional) +// tense - "present" | "past" | "future" (default: "present") +// aspect - "simple" | "progressive" | "perfect" (default: "simple") + +// ── Include all sub-modules (concatenated at build time via nlg.el) ──────────────────────────── +// In El, there is no import system for standalone programs, so the sub-modules +// are inlined above this file in the build. When tests include nlg.el, they +// first include vocabulary.el, morphology.el, grammar.el, realizer.el, +// then this file. + +// ── JSON helpers ────────────────────────────────────────────────────────────── + +fn sem_get(json: String, key: String) -> String { + let val: String = json_get(json, key) + return val +} + +// ── Public API ──────────────────────────────────────────────────────────────── + +// Generate an English sentence from a semantic form JSON string. +fn generate(semantic_form_json: String) -> String { + let intent: String = sem_get(semantic_form_json, "intent") + let agent: String = sem_get(semantic_form_json, "agent") + let predicate: String = sem_get(semantic_form_json, "predicate") + let patient: String = sem_get(semantic_form_json, "patient") + 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") + + // Build the slot map for the realizer + let form: [String] = native_list_empty() + let form = native_list_append(form, "intent") + let form = native_list_append(form, intent) + let form = native_list_append(form, "agent") + let form = native_list_append(form, agent) + let form = native_list_append(form, "predicate") + 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, "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) + + return realize(form) +} diff --git a/elp/src/realizer.el b/elp/src/realizer.el new file mode 100644 index 0000000..3ece503 --- /dev/null +++ b/elp/src/realizer.el @@ -0,0 +1,261 @@ +// realizer.el - Syntactic realizer: semantic form -> English text. +// +// Takes a SemanticForm (represented as a [String] slot map) and produces +// a well-formed English sentence, handling: +// - verb conjugation (tense, aspect, person/number agreement) +// - do-support for questions +// - progressive and perfect aspect periphrasis +// - determiner-noun agreement (a/an) +// - capitalization and final punctuation +// +// Depends on: morphology (verb_form, agree_determiner) +// grammar (generate_tree, render_tree, slots_*, build_np, build_pp, etc.) + +// ── Semantic form slot keys ─────────────────────────────────────────────────── +// +// intent - "assert" | "question" | "command" +// agent - subject referent string (pronoun or "det noun" phrase) +// predicate - verb base form +// patient - object referent string (optional) +// location - prepositional phrase string, e.g. "in the park" (optional) +// tense - "present" | "past" | "future" +// aspect - "simple" | "progressive" | "perfect" +// +// Additional computed slots used internally: +// agent_person - "first" | "second" | "third" +// agent_number - "singular" | "plural" +// verb_surf - conjugated verb surface form +// aux_surf - conjugated auxiliary surface form + +// ── Agent agreement analysis ────────────────────────────────────────────────── + +fn agent_person(agent: String) -> String { + if str_eq(agent, "I") { return "first" } + if str_eq(agent, "me") { return "first" } + if str_eq(agent, "we") { return "first" } + if str_eq(agent, "us") { return "first" } + if str_eq(agent, "you") { return "second" } + return "third" +} + +fn agent_number(agent: String) -> String { + if str_eq(agent, "I") { return "singular" } + if str_eq(agent, "me") { return "singular" } + if str_eq(agent, "he") { return "singular" } + if str_eq(agent, "him") { return "singular" } + if str_eq(agent, "she") { return "singular" } + if str_eq(agent, "her") { return "singular" } + if str_eq(agent, "it") { return "singular" } + if str_eq(agent, "you") { return "singular" } + if str_eq(agent, "we") { return "plural" } + if str_eq(agent, "us") { return "plural" } + if str_eq(agent, "they") { return "plural" } + if str_eq(agent, "them") { return "plural" } + // For noun phrases: check for plural determiner or known plural nouns + // Default to singular for simple noun phrases + return "singular" +} + +// ── NP realization ──────────────────────────────────────────────────────────── + +fn realize_np(referent: String, number: String) -> String { + return referent +} + +// ── VP realization ──────────────────────────────────────────────────────────── +// +// Returns the surface verb string (possibly with auxiliary) for a VP. +// aspect: "simple" | "progressive" | "perfect" +// tense: "present" | "past" | "future" +// person: "first" | "second" | "third" +// number: "singular" | "plural" +// Returns [main_verb_surface, aux_surface_or_empty] + +fn realize_vp(base_verb: String, tense: String, aspect: String, person: String, number: String) -> [String] { + let empty_aux: String = "" + + // ── future: will + base ──────────────────────────────────────────────── + if str_eq(tense, "future") { + let result: [String] = native_list_empty() + let result = native_list_append(result, base_verb) + let result = native_list_append(result, "will") + return result + } + + // ── progressive: be + gerund ─────────────────────────────────────────── + if str_eq(aspect, "progressive") { + let gerund: String = verb_form(base_verb, "progressive", person, number) + // be auxiliary conjugated for tense + let be_aux: String = verb_form("be", tense, person, number) + let result: [String] = native_list_empty() + let result = native_list_append(result, gerund) + let result = native_list_append(result, be_aux) + return result + } + + // ── perfect: have + past participle ─────────────────────────────────── + if str_eq(aspect, "perfect") { + let pp: String = verb_form(base_verb, "perfect", person, number) + // have auxiliary conjugated for tense+agreement + let have_form: String = verb_form("have", tense, person, number) + let result: [String] = native_list_empty() + let result = native_list_append(result, pp) + let result = native_list_append(result, have_form) + return result + } + + // ── simple ───────────────────────────────────────────────────────────── + let surf: String = verb_form(base_verb, tense, person, number) + let result: [String] = native_list_empty() + let result = native_list_append(result, surf) + let result = native_list_append(result, empty_aux) + return result +} + +// ── Do-support for questions ────────────────────────────────────────────────── +// +// "Do you see the dog?" uses do-support for non-be verbs in simple aspect. +// Returns [do_aux_surface, bare_verb_base] + +fn realize_do_support(base_verb: String, tense: String, person: String, number: String) -> [String] { + // For "be" verbs: invert directly, no do-support + if str_eq(base_verb, "be") { + let be_form: String = verb_form("be", tense, person, number) + let result: [String] = native_list_empty() + let result = native_list_append(result, be_form) + let result = native_list_append(result, "") + return result + } + // For modal/have-perfect: invert the auxiliary directly + // For simple tense: do-support + let do_form: String = verb_form("do", tense, person, number) + let result: [String] = native_list_empty() + let result = native_list_append(result, do_form) + let result = native_list_append(result, base_verb) + return result +} + +// ── Capitalization and punctuation ──────────────────────────────────────────── + +fn capitalize_first(s: String) -> String { + let n: Int = str_len(s) + if n == 0 { + return s + } + let first: String = str_slice(s, 0, 1) + let rest: String = str_slice(s, 1, n) + return str_to_upper(first) + rest +} + +fn add_punct(s: String, intent: String) -> String { + if str_eq(intent, "question") { + return s + "?" + } + if str_eq(intent, "command") { + return s + "." + } + return s + "." +} + +// ── Main realization ────────────────────────────────────────────────────────── + +fn realize(form: [String]) -> String { + let intent: String = slots_get(form, "intent") + let agent: String = slots_get(form, "agent") + let predicate: String = slots_get(form, "predicate") + let patient: String = slots_get(form, "patient") + let location: String = slots_get(form, "location") + let tense_raw: String = slots_get(form, "tense") + let aspect_raw: String = slots_get(form, "aspect") + + let tense: String = tense_raw + if str_eq(tense, "") { + let tense = "present" + } + let aspect: String = aspect_raw + if str_eq(aspect, "") { + let aspect = "simple" + } + + let person: String = agent_person(agent) + let number: String = agent_number(agent) + + // ── Command (imperative) ────────────────────────────────────────────────── + if str_eq(intent, "command") { + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, predicate) + if !str_eq(patient, "") { + let parts = native_list_append(parts, patient) + } + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "command") + } + + // ── Question (yes/no) ───────────────────────────────────────────────────── + if str_eq(intent, "question") { + let do_pair: [String] = realize_do_support(predicate, tense, person, number) + let aux_surf: String = native_list_get(do_pair, 0) + let verb_bare: String = native_list_get(do_pair, 1) + + // For progressive/perfect questions, use the aspect auxiliary instead + let use_verb: String = verb_bare + let use_aux: String = aux_surf + + if str_eq(aspect, "progressive") { + let vp_pair: [String] = realize_vp(predicate, tense, "progressive", person, number) + let gerund: String = native_list_get(vp_pair, 0) + let be_aux: String = native_list_get(vp_pair, 1) + let use_aux = be_aux + let use_verb = gerund + } + + if str_eq(aspect, "perfect") { + let vp_pair: [String] = realize_vp(predicate, tense, "perfect", person, number) + let pp: String = native_list_get(vp_pair, 0) + let have_aux: String = native_list_get(vp_pair, 1) + let use_aux = have_aux + let use_verb = pp + } + + // Build: Aux Agent Verb [Patient] [Location] ? + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, use_aux) + let parts = native_list_append(parts, agent) + if str_eq(use_verb, "") { + // be inversion: Aux already is the be form, no separate verb + } else { + let parts = native_list_append(parts, use_verb) + } + if !str_eq(patient, "") { + let parts = native_list_append(parts, patient) + } + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "question") + } + + // ── Assertion (declarative) ─────────────────────────────────────────────── + let vp_pair: [String] = realize_vp(predicate, tense, aspect, person, number) + let verb_surf: String = native_list_get(vp_pair, 0) + let aux_surf: String = native_list_get(vp_pair, 1) + + let parts: [String] = native_list_empty() + let parts = native_list_append(parts, agent) + if !str_eq(aux_surf, "") { + let parts = native_list_append(parts, aux_surf) + } + let parts = native_list_append(parts, verb_surf) + if !str_eq(patient, "") { + let parts = native_list_append(parts, patient) + } + if !str_eq(location, "") { + let parts = native_list_append(parts, location) + } + let sentence: String = str_join(parts, " ") + return add_punct(capitalize_first(sentence), "assert") +} diff --git a/elp/src/vocabulary.el b/elp/src/vocabulary.el new file mode 100644 index 0000000..3a6eddb --- /dev/null +++ b/elp/src/vocabulary.el @@ -0,0 +1,328 @@ +// vocabulary.el - Vocabulary query interface with inline seed data. +// +// Represents lexical entries for English words with POS, morphological forms, +// semantic class, and synonyms. Inline data covers the vocabulary needed for +// grammar tests. The long-term backing store is the Neuron engram (queried at +// runtime), but inline data is used for bootstrapping. +// +// POS values: "noun" | "verb" | "adjective" | "adverb" | "determiner" | +// "preposition" | "pronoun" | "conjunction" | "auxiliary" + +// ── LexEntry: a single lexeme ───────────────────────────────────────────────── +// +// forms meaning: +// noun: [singular, plural] +// verb: [base, 3sg, past, pastpart, gerund] +// pronoun: [nominative, accusative, genitive] +// other: [word] + +fn lex_word(entry: [String]) -> String { + return native_list_get(entry, 0) +} + +fn lex_pos(entry: [String]) -> String { + return native_list_get(entry, 1) +} + +fn lex_form(entry: [String], idx: Int) -> String { + // forms start at index 2 + let n: Int = native_list_len(entry) + let real_idx: Int = idx + 2 + if real_idx >= n { + return native_list_get(entry, 0) + } + return native_list_get(entry, real_idx) +} + +fn lex_class(entry: [String]) -> String { + let n: Int = native_list_len(entry) + let last: Int = n - 1 + return native_list_get(entry, last) +} + +// Build a vocab entry: [word, pos, form0, form1, ..., semantic_class] +fn make_entry(word: String, pos: String, f0: String, f1: String, f2: String, f3: String, f4: String, cls: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, word) + let r = native_list_append(r, pos) + let r = native_list_append(r, f0) + let r = native_list_append(r, f1) + let r = native_list_append(r, f2) + let r = native_list_append(r, f3) + let r = native_list_append(r, f4) + let r = native_list_append(r, cls) + return r +} + +fn make_entry2(word: String, pos: String, f0: String, f1: String, cls: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, word) + let r = native_list_append(r, pos) + let r = native_list_append(r, f0) + let r = native_list_append(r, f1) + let r = native_list_append(r, cls) + return r +} + +fn make_entry3(word: String, pos: String, f0: String, f1: String, f2: String, cls: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, word) + let r = native_list_append(r, pos) + let r = native_list_append(r, f0) + let r = native_list_append(r, f1) + let r = native_list_append(r, f2) + let r = native_list_append(r, cls) + return r +} + +fn make_entry1(word: String, pos: String, f0: String, cls: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, word) + let r = native_list_append(r, pos) + let r = native_list_append(r, f0) + let r = native_list_append(r, cls) + return r +} + +// ── Seed vocabulary ─────────────────────────────────────────────────────────── + +fn build_vocab() -> [[String]] { + let v: [[String]] = native_list_empty() + + // ── Pronouns [nominative, accusative, genitive] ─────────────────────────── + let v = native_list_append(v, make_entry3("I", "pronoun", "I", "me", "my", "person-first-sg")) + let v = native_list_append(v, make_entry3("you", "pronoun", "you", "you", "your", "person-second")) + let v = native_list_append(v, make_entry3("he", "pronoun", "he", "him", "his", "person-third-sg-m")) + let v = native_list_append(v, make_entry3("she", "pronoun", "she", "her", "her", "person-third-sg-f")) + let v = native_list_append(v, make_entry3("it", "pronoun", "it", "it", "its", "person-third-sg-n")) + let v = native_list_append(v, make_entry3("we", "pronoun", "we", "us", "our", "person-first-pl")) + let v = native_list_append(v, make_entry3("they", "pronoun", "they", "them", "their", "person-third-pl")) + + // ── Determiners [word] ──────────────────────────────────────────────────── + let v = native_list_append(v, make_entry1("a", "determiner", "a", "indefinite")) + let v = native_list_append(v, make_entry1("an", "determiner", "an", "indefinite")) + let v = native_list_append(v, make_entry1("the", "determiner", "the", "definite")) + let v = native_list_append(v, make_entry1("some", "determiner", "some", "indefinite-pl")) + let v = native_list_append(v, make_entry1("this", "determiner", "this", "demonstrative-sg")) + let v = native_list_append(v, make_entry1("that", "determiner", "that", "demonstrative-sg")) + let v = native_list_append(v, make_entry1("these","determiner", "these","demonstrative-pl")) + let v = native_list_append(v, make_entry1("those","determiner", "those","demonstrative-pl")) + + // ── Prepositions [word] ─────────────────────────────────────────────────── + let v = native_list_append(v, make_entry1("in", "preposition", "in", "location")) + let v = native_list_append(v, make_entry1("on", "preposition", "on", "location")) + let v = native_list_append(v, make_entry1("at", "preposition", "at", "location")) + let v = native_list_append(v, make_entry1("to", "preposition", "to", "direction")) + let v = native_list_append(v, make_entry1("for", "preposition", "for", "purpose")) + let v = native_list_append(v, make_entry1("of", "preposition", "of", "relation")) + let v = native_list_append(v, make_entry1("with", "preposition", "with", "accompaniment")) + let v = native_list_append(v, make_entry1("from", "preposition", "from", "source")) + let v = native_list_append(v, make_entry1("by", "preposition", "by", "agent")) + let v = native_list_append(v, make_entry1("into", "preposition", "into", "direction")) + + // ── Auxiliaries [base, 3sg, past, pastpart, gerund] ─────────────────────── + let v = native_list_append(v, make_entry("is", "auxiliary", "be", "is", "was", "been", "being", "copula")) + let v = native_list_append(v, make_entry("are", "auxiliary", "be", "is", "was", "been", "being", "copula")) + let v = native_list_append(v, make_entry("was", "auxiliary", "be", "is", "was", "been", "being", "copula-past")) + let v = native_list_append(v, make_entry("were", "auxiliary", "be", "is", "were", "been", "being", "copula-past")) + let v = native_list_append(v, make_entry("has", "auxiliary", "have", "has", "had", "had", "having", "perfect")) + let v = native_list_append(v, make_entry("have", "auxiliary", "have", "has", "had", "had", "having", "perfect")) + let v = native_list_append(v, make_entry("had", "auxiliary", "have", "has", "had", "had", "having", "perfect-past")) + let v = native_list_append(v, make_entry("will", "auxiliary", "will", "will", "would", "would", "willing", "future")) + let v = native_list_append(v, make_entry("can", "auxiliary", "can", "can", "could", "could", "canning", "modal")) + let v = native_list_append(v, make_entry("could", "auxiliary", "can", "can", "could", "could", "canning", "modal-past")) + let v = native_list_append(v, make_entry("would", "auxiliary", "will", "will", "would", "would", "willing", "modal-cond")) + let v = native_list_append(v, make_entry("do", "auxiliary", "do", "does", "did", "done", "doing", "do-support")) + let v = native_list_append(v, make_entry("does", "auxiliary", "do", "does", "did", "done", "doing", "do-support")) + let v = native_list_append(v, make_entry("did", "auxiliary", "do", "does", "did", "done", "doing", "do-support-past")) + + // ── Nouns [singular, plural] ────────────────────────────────────────────── + let v = native_list_append(v, make_entry2("cat", "noun", "cat", "cats", "animal")) + let v = native_list_append(v, make_entry2("dog", "noun", "dog", "dogs", "animal")) + let v = native_list_append(v, make_entry2("bird", "noun", "bird", "birds", "animal")) + let v = native_list_append(v, make_entry2("fish", "noun", "fish", "fish", "animal")) + let v = native_list_append(v, make_entry2("horse", "noun", "horse", "horses", "animal")) + let v = native_list_append(v, make_entry2("house", "noun", "house", "houses", "building")) + let v = native_list_append(v, make_entry2("book", "noun", "book", "books", "object")) + let v = native_list_append(v, make_entry2("table", "noun", "table", "tables", "furniture")) + let v = native_list_append(v, make_entry2("chair", "noun", "chair", "chairs", "furniture")) + let v = native_list_append(v, make_entry2("door", "noun", "door", "doors", "structure")) + let v = native_list_append(v, make_entry2("window", "noun", "window", "windows", "structure")) + let v = native_list_append(v, make_entry2("city", "noun", "city", "cities", "place")) + let v = native_list_append(v, make_entry2("park", "noun", "park", "parks", "place")) + let v = native_list_append(v, make_entry2("school", "noun", "school", "schools", "place")) + let v = native_list_append(v, make_entry2("store", "noun", "store", "stores", "place")) + let v = native_list_append(v, make_entry2("road", "noun", "road", "roads", "place")) + let v = native_list_append(v, make_entry2("box", "noun", "box", "boxes", "container")) + let v = native_list_append(v, make_entry2("child", "noun", "child", "children", "person")) + let v = native_list_append(v, make_entry2("person", "noun", "person", "people", "person")) + let v = native_list_append(v, make_entry2("man", "noun", "man", "men", "person")) + let v = native_list_append(v, make_entry2("woman", "noun", "woman", "women", "person")) + let v = native_list_append(v, make_entry2("tree", "noun", "tree", "trees", "plant")) + let v = native_list_append(v, make_entry2("flower", "noun", "flower", "flowers", "plant")) + let v = native_list_append(v, make_entry2("water", "noun", "water", "waters", "substance")) + let v = native_list_append(v, make_entry2("food", "noun", "food", "foods", "substance")) + let v = native_list_append(v, make_entry2("time", "noun", "time", "times", "abstract")) + let v = native_list_append(v, make_entry2("day", "noun", "day", "days", "time")) + let v = native_list_append(v, make_entry2("night", "noun", "night", "nights", "time")) + let v = native_list_append(v, make_entry2("home", "noun", "home", "homes", "place")) + + // ── Verbs [base, 3sg, past, pastpart, gerund] ───────────────────────────── + let v = native_list_append(v, make_entry("run", "verb", "run", "runs", "ran", "run", "running", "motion")) + let v = native_list_append(v, make_entry("walk", "verb", "walk", "walks", "walked", "walked", "walking", "motion")) + let v = native_list_append(v, make_entry("go", "verb", "go", "goes", "went", "gone", "going", "motion")) + let v = native_list_append(v, make_entry("come", "verb", "come", "comes", "came", "come", "coming", "motion")) + let v = native_list_append(v, make_entry("see", "verb", "see", "sees", "saw", "seen", "seeing", "perception")) + let v = native_list_append(v, make_entry("hear", "verb", "hear", "hears", "heard", "heard", "hearing", "perception")) + let v = native_list_append(v, make_entry("look", "verb", "look", "looks", "looked", "looked", "looking", "perception")) + let v = native_list_append(v, make_entry("eat", "verb", "eat", "eats", "ate", "eaten", "eating", "action")) + let v = native_list_append(v, make_entry("drink", "verb", "drink", "drinks", "drank", "drunk", "drinking", "action")) + let v = native_list_append(v, make_entry("sleep", "verb", "sleep", "sleeps", "slept", "slept", "sleeping", "state")) + let v = native_list_append(v, make_entry("sit", "verb", "sit", "sits", "sat", "sat", "sitting", "posture")) + let v = native_list_append(v, make_entry("stand", "verb", "stand", "stands", "stood", "stood", "standing", "posture")) + let v = native_list_append(v, make_entry("give", "verb", "give", "gives", "gave", "given", "giving", "transfer")) + let v = native_list_append(v, make_entry("take", "verb", "take", "takes", "took", "taken", "taking", "transfer")) + let v = native_list_append(v, make_entry("make", "verb", "make", "makes", "made", "made", "making", "creation")) + let v = native_list_append(v, make_entry("put", "verb", "put", "puts", "put", "put", "putting", "placement")) + let v = native_list_append(v, make_entry("find", "verb", "find", "finds", "found", "found", "finding", "discovery")) + let v = native_list_append(v, make_entry("know", "verb", "know", "knows", "knew", "known", "knowing", "cognition")) + let v = native_list_append(v, make_entry("think", "verb", "think", "thinks", "thought","thought","thinking", "cognition")) + let v = native_list_append(v, make_entry("say", "verb", "say", "says", "said", "said", "saying", "communication")) + let v = native_list_append(v, make_entry("tell", "verb", "tell", "tells", "told", "told", "telling", "communication")) + let v = native_list_append(v, make_entry("ask", "verb", "ask", "asks", "asked", "asked", "asking", "communication")) + let v = native_list_append(v, make_entry("like", "verb", "like", "likes", "liked", "liked", "liking", "emotion")) + let v = native_list_append(v, make_entry("love", "verb", "love", "loves", "loved", "loved", "loving", "emotion")) + let v = native_list_append(v, make_entry("want", "verb", "want", "wants", "wanted", "wanted", "wanting", "desire")) + let v = native_list_append(v, make_entry("need", "verb", "need", "needs", "needed", "needed", "needing", "desire")) + let v = native_list_append(v, make_entry("have", "verb", "have", "has", "had", "had", "having", "possession")) + let v = native_list_append(v, make_entry("hold", "verb", "hold", "holds", "held", "held", "holding", "possession")) + let v = native_list_append(v, make_entry("open", "verb", "open", "opens", "opened", "opened", "opening", "action")) + let v = native_list_append(v, make_entry("close", "verb", "close", "closes", "closed", "closed", "closing", "action")) + let v = native_list_append(v, make_entry("write", "verb", "write", "writes", "wrote", "written","writing", "action")) + let v = native_list_append(v, make_entry("read", "verb", "read", "reads", "read", "read", "reading", "action")) + let v = native_list_append(v, make_entry("build", "verb", "build", "builds", "built", "built", "building", "creation")) + let v = native_list_append(v, make_entry("live", "verb", "live", "lives", "lived", "lived", "living", "state")) + let v = native_list_append(v, make_entry("work", "verb", "work", "works", "worked", "worked", "working", "activity")) + let v = native_list_append(v, make_entry("play", "verb", "play", "plays", "played", "played", "playing", "activity")) + let v = native_list_append(v, make_entry("help", "verb", "help", "helps", "helped", "helped", "helping", "activity")) + + // ── Adjectives [word] ───────────────────────────────────────────────────── + let v = native_list_append(v, make_entry1("big", "adjective", "big", "size")) + let v = native_list_append(v, make_entry1("small", "adjective", "small", "size")) + let v = native_list_append(v, make_entry1("large", "adjective", "large", "size")) + let v = native_list_append(v, make_entry1("little", "adjective", "little", "size")) + let v = native_list_append(v, make_entry1("old", "adjective", "old", "age")) + let v = native_list_append(v, make_entry1("new", "adjective", "new", "age")) + let v = native_list_append(v, make_entry1("young", "adjective", "young", "age")) + let v = native_list_append(v, make_entry1("good", "adjective", "good", "quality")) + let v = native_list_append(v, make_entry1("bad", "adjective", "bad", "quality")) + let v = native_list_append(v, make_entry1("fast", "adjective", "fast", "speed")) + let v = native_list_append(v, make_entry1("slow", "adjective", "slow", "speed")) + let v = native_list_append(v, make_entry1("hot", "adjective", "hot", "temperature")) + let v = native_list_append(v, make_entry1("cold", "adjective", "cold", "temperature")) + let v = native_list_append(v, make_entry1("happy", "adjective", "happy", "emotion")) + let v = native_list_append(v, make_entry1("sad", "adjective", "sad", "emotion")) + let v = native_list_append(v, make_entry1("red", "adjective", "red", "color")) + let v = native_list_append(v, make_entry1("blue", "adjective", "blue", "color")) + let v = native_list_append(v, make_entry1("green", "adjective", "green", "color")) + let v = native_list_append(v, make_entry1("white", "adjective", "white", "color")) + let v = native_list_append(v, make_entry1("black", "adjective", "black", "color")) + let v = native_list_append(v, make_entry1("long", "adjective", "long", "dimension")) + let v = native_list_append(v, make_entry1("short", "adjective", "short", "dimension")) + let v = native_list_append(v, make_entry1("beautiful","adjective", "beautiful","appearance")) + let v = native_list_append(v, make_entry1("bright", "adjective", "bright", "appearance")) + let v = native_list_append(v, make_entry1("dark", "adjective", "dark", "appearance")) + + return v +} + +// ── Vocabulary cache ────────────────────────────────────────────────────────── +// The vocab list is built once and reused across queries. +// We use a simple linear scan (adequate for ~100 entries). + +fn get_vocab() -> [[String]] { + return build_vocab() +} + +// ── Query functions ─────────────────────────────────────────────────────────── + +// Returns the entry whose word (index 0) matches, or an empty list if not found. +fn vocab_lookup(word: String) -> [String] { + let vocab: [[String]] = get_vocab() + let n: Int = native_list_len(vocab) + let i: Int = 0 + while i < n { + let entry: [String] = native_list_get(vocab, i) + let w: String = native_list_get(entry, 0) + if str_eq(w, word) { + return entry + } + let i = i + 1 + } + let empty: [String] = native_list_empty() + return empty +} + +// Returns all entries whose pos (index 1) matches. +fn vocab_by_pos(pos: String) -> [[String]] { + let vocab: [[String]] = get_vocab() + let n: Int = native_list_len(vocab) + let result: [[String]] = native_list_empty() + let i: Int = 0 + while i < n { + let entry: [String] = native_list_get(vocab, i) + let p: String = native_list_get(entry, 1) + if str_eq(p, pos) { + let result = native_list_append(result, entry) + } + let i = i + 1 + } + return result +} + +// Returns all entries whose semantic class (last element) matches or starts with cls. +fn vocab_by_class(cls: String) -> [[String]] { + let vocab: [[String]] = get_vocab() + let n: Int = native_list_len(vocab) + let result: [[String]] = native_list_empty() + let i: Int = 0 + while i < n { + let entry: [String] = native_list_get(vocab, i) + let m: Int = native_list_len(entry) + let c: String = native_list_get(entry, m - 1) + if str_eq(c, cls) { + let result = native_list_append(result, entry) + } + let i = i + 1 + } + return result +} + +// ── Convenience accessors for a retrieved entry ─────────────────────────────── + +// Is this entry non-empty (i.e., found)? +fn entry_found(entry: [String]) -> Bool { + let n: Int = native_list_len(entry) + if n > 0 { + return true + } + return false +} + +fn entry_word(entry: [String]) -> String { + return native_list_get(entry, 0) +} + +fn entry_pos(entry: [String]) -> String { + return native_list_get(entry, 1) +} + +// Get the Nth morphological form (0-indexed, starts at position 2 in the list). +fn entry_form(entry: [String], n: Int) -> String { + let real: Int = n + 2 + let total: Int = native_list_len(entry) + if real >= total { + return native_list_get(entry, 0) + } + return native_list_get(entry, real) +} diff --git a/elp/tests/examples/basic-sentence.el b/elp/tests/examples/basic-sentence.el new file mode 100644 index 0000000..c7484cf --- /dev/null +++ b/elp/tests/examples/basic-sentence.el @@ -0,0 +1,24 @@ +// basic-sentence.el - Realize "I see the cat." +// +// SemanticForm: assert, agent=I, predicate=see, patient=the cat, present simple. + +fn run_test() -> String { + let form: [String] = native_list_empty() + let form = native_list_append(form, "intent") + let form = native_list_append(form, "assert") + let form = native_list_append(form, "agent") + let form = native_list_append(form, "I") + let form = native_list_append(form, "predicate") + let form = native_list_append(form, "see") + let form = native_list_append(form, "patient") + let form = native_list_append(form, "the cat") + let form = native_list_append(form, "location") + let form = native_list_append(form, "") + let form = native_list_append(form, "tense") + let form = native_list_append(form, "present") + let form = native_list_append(form, "aspect") + let form = native_list_append(form, "simple") + return realize(form) +} + +println(run_test()) diff --git a/elp/tests/examples/command.el b/elp/tests/examples/command.el new file mode 100644 index 0000000..8b1a1f2 --- /dev/null +++ b/elp/tests/examples/command.el @@ -0,0 +1,24 @@ +// command.el - Realize "Go home." +// +// SemanticForm: command, predicate=go, location=home. + +fn run_test() -> String { + let form: [String] = native_list_empty() + let form = native_list_append(form, "intent") + let form = native_list_append(form, "command") + let form = native_list_append(form, "agent") + let form = native_list_append(form, "") + let form = native_list_append(form, "predicate") + let form = native_list_append(form, "go") + let form = native_list_append(form, "patient") + let form = native_list_append(form, "") + let form = native_list_append(form, "location") + let form = native_list_append(form, "home") + let form = native_list_append(form, "tense") + let form = native_list_append(form, "present") + let form = native_list_append(form, "aspect") + let form = native_list_append(form, "simple") + return realize(form) +} + +println(run_test()) diff --git a/elp/tests/examples/past-tense.el b/elp/tests/examples/past-tense.el new file mode 100644 index 0000000..15e48b4 --- /dev/null +++ b/elp/tests/examples/past-tense.el @@ -0,0 +1,24 @@ +// past-tense.el - Realize "She ran in the park." +// +// SemanticForm: assert, agent=she, predicate=run, location=in the park, past simple. + +fn run_test() -> String { + let form: [String] = native_list_empty() + let form = native_list_append(form, "intent") + let form = native_list_append(form, "assert") + let form = native_list_append(form, "agent") + let form = native_list_append(form, "she") + let form = native_list_append(form, "predicate") + let form = native_list_append(form, "run") + let form = native_list_append(form, "patient") + let form = native_list_append(form, "") + let form = native_list_append(form, "location") + let form = native_list_append(form, "in the park") + let form = native_list_append(form, "tense") + let form = native_list_append(form, "past") + let form = native_list_append(form, "aspect") + let form = native_list_append(form, "simple") + return realize(form) +} + +println(run_test()) diff --git a/elp/tests/examples/plural-noun.el b/elp/tests/examples/plural-noun.el new file mode 100644 index 0000000..7d04874 --- /dev/null +++ b/elp/tests/examples/plural-noun.el @@ -0,0 +1,16 @@ +// plural-noun.el - Test noun pluralization. +// +// Expected: "cats|children|boxes|cities|fish|leaves" + +fn run_test() -> String { + let r: [String] = native_list_empty() + let r = native_list_append(r, pluralize("cat")) + let r = native_list_append(r, pluralize("child")) + let r = native_list_append(r, pluralize("box")) + let r = native_list_append(r, pluralize("city")) + let r = native_list_append(r, pluralize("fish")) + let r = native_list_append(r, pluralize("leaf")) + return str_join(r, "|") +} + +println(run_test()) diff --git a/elp/tests/examples/question.el b/elp/tests/examples/question.el new file mode 100644 index 0000000..1fa62f0 --- /dev/null +++ b/elp/tests/examples/question.el @@ -0,0 +1,24 @@ +// question.el - Realize "Do you see the dog?" +// +// SemanticForm: question, agent=you, predicate=see, patient=the dog, present simple. + +fn run_test() -> String { + let form: [String] = native_list_empty() + let form = native_list_append(form, "intent") + let form = native_list_append(form, "question") + let form = native_list_append(form, "agent") + let form = native_list_append(form, "you") + let form = native_list_append(form, "predicate") + let form = native_list_append(form, "see") + let form = native_list_append(form, "patient") + let form = native_list_append(form, "the dog") + let form = native_list_append(form, "location") + let form = native_list_append(form, "") + let form = native_list_append(form, "tense") + let form = native_list_append(form, "present") + let form = native_list_append(form, "aspect") + let form = native_list_append(form, "simple") + return realize(form) +} + +println(run_test()) diff --git a/elp/tests/examples/verb-conjugation.el b/elp/tests/examples/verb-conjugation.el new file mode 100644 index 0000000..e1e2163 --- /dev/null +++ b/elp/tests/examples/verb-conjugation.el @@ -0,0 +1,15 @@ +// verb-conjugation.el - Test verb conjugation. +// +// Expected: "ran|walked|is|am|sleeping" + +fn run_test() -> String { + let r: [String] = native_list_empty() + let r = native_list_append(r, verb_form("run", "past", "third", "singular")) + let r = native_list_append(r, verb_form("walk", "past", "third", "singular")) + let r = native_list_append(r, verb_form("be", "present", "third", "singular")) + let r = native_list_append(r, verb_form("be", "present", "first", "singular")) + let r = native_list_append(r, verb_form("sleep","progressive","third","singular")) + return str_join(r, "|") +} + +println(run_test()) diff --git a/elp/tests/run.sh b/elp/tests/run.sh new file mode 100755 index 0000000..61ce4ca --- /dev/null +++ b/elp/tests/run.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# run.sh - build and execute the nlg/ acceptance corpus. +# +# Each examples/.el is a self-contained El program that calls NLG +# functions and prints a deterministic result line. Because El has no +# import system, the runner concatenates all NLG source modules above each +# test file before handing it to elc. +# +# Module load order: +# morphology.el (standalone) +# vocabulary.el (standalone) +# grammar.el (uses slot helpers) +# realizer.el (uses morphology + grammar) +# nlg.el (public API) +# .el (calls the above) + +set -uo pipefail +cd "$(dirname "$0")" + +EL_HOME="${EL_HOME:-$(cd ../.. && pwd)/el}" +ELC="${ELC:-${EL_HOME}/dist/platform/elc}" +RUNTIME_DIR="${EL_HOME}/el-compiler/runtime" +SRC_DIR="$(cd .. && pwd)/src" + +if [ ! -x "${ELC}" ]; then + echo "elc not found at ${ELC}" >&2 + exit 1 +fi + +# NLG source modules in dependency order +NLG_MODULES=( + "${SRC_DIR}/morphology.el" + "${SRC_DIR}/vocabulary.el" + "${SRC_DIR}/grammar.el" + "${SRC_DIR}/realizer.el" + "${SRC_DIR}/nlg.el" +) + +PASS=0 +FAIL=0 +FAILED_NAMES=() + +run_nlg_case() { + local name="$1" + local src="$2" + local expected="$3" + + local combined_src + local out_c + local out_bin + combined_src="$(mktemp -t nlg_test.XXXXXX).el" + out_c="$(mktemp -t nlg_test.XXXXXX).c" + out_bin="$(mktemp -t nlg_test.XXXXXX)" + + # Concatenate modules + test file + for mod in "${NLG_MODULES[@]}"; do + cat "${mod}" >> "${combined_src}" + echo "" >> "${combined_src}" + done + cat "${src}" >> "${combined_src}" + + if ! "${ELC}" "${combined_src}" > "${out_c}" 2>/tmp/nlg_test.elc.err; then + echo "FAIL ${name} - elc emit failed:" + cat /tmp/nlg_test.elc.err | sed 's/^/ /' + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + rm -f "${combined_src}" "${out_c}" "${out_bin}" + return + fi + + if ! cc -O2 -I "${RUNTIME_DIR}" "${out_c}" "${RUNTIME_DIR}/el_runtime.c" \ + -lcurl -lpthread -o "${out_bin}" 2>/tmp/nlg_test.cc.err; then + echo "FAIL ${name} - cc failed:" + cat /tmp/nlg_test.cc.err | sed 's/^/ /' + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + rm -f "${combined_src}" "${out_c}" "${out_bin}" + return + fi + + local got + got="$("${out_bin}" 2>&1)" + + if [ "${got}" = "${expected}" ]; then + echo "PASS ${name}" + PASS=$((PASS+1)) + else + echo "FAIL ${name} expected: '${expected}', got: '${got}'" + FAIL=$((FAIL+1)) + FAILED_NAMES+=("${name}") + fi + + rm -f "${combined_src}" "${out_c}" "${out_bin}" +} + +echo "==> Running nlg acceptance corpus" +echo + +run_nlg_case "basic-sentence" examples/basic-sentence.el "I see the cat." +run_nlg_case "past-tense" examples/past-tense.el "She ran in the park." +run_nlg_case "question" examples/question.el "Do you see the dog?" +run_nlg_case "command" examples/command.el "Go home." +run_nlg_case "plural-noun" examples/plural-noun.el "cats|children|boxes|cities|fish|leaves" +run_nlg_case "verb-conjugation" examples/verb-conjugation.el "ran|walked|is|am|sleeping" + +echo +echo "${PASS} passed, ${FAIL} failed" +if [ "${FAIL}" -gt 0 ]; then + echo "failed: ${FAILED_NAMES[*]}" + exit 1 +fi +exit 0