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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user