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)
This commit is contained in:
Will Anderson
2026-05-02 14:16:23 -05:00
commit 1432c56cf7
13 changed files with 1956 additions and 0 deletions
+9
View File
@@ -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"
}
+495
View File
@@ -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)
}
+566
View File
@@ -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
}
+58
View File
@@ -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)
}
+261
View File
@@ -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")
}
+328
View File
@@ -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)
}
+24
View File
@@ -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())
+24
View File
@@ -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())
+24
View File
@@ -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())
+16
View File
@@ -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())
+24
View File
@@ -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())
+15
View File
@@ -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())
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# run.sh - build and execute the nlg/ acceptance corpus.
#
# Each examples/<case>.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)
# <test>.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