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
+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
}