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,9 @@
|
||||
package "nlg" {
|
||||
version "0.1.0"
|
||||
description "Native El natural language generation - vocabulary, grammar, realizer, morphology"
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
build {
|
||||
entry "src/nlg.el"
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
// grammar.el - Context-free grammar for English.
|
||||
//
|
||||
// Grammar rules are stored as lists: [id, lhs, rhs_part0, rhs_part1, ...]
|
||||
// Tree nodes are stored as lists: [label, word, child0, child1, ...]
|
||||
// where child slots are also lists (nested tree nodes).
|
||||
//
|
||||
// This module provides:
|
||||
// - A catalog of English grammar rules (S, NP, VP, PP)
|
||||
// - A generator that fills a rule skeleton with semantic slots
|
||||
//
|
||||
// Slots are passed as a flat string map encoded as a list:
|
||||
// ["key1", "val1", "key2", "val2", ...]
|
||||
//
|
||||
// Depends on: nothing (standalone)
|
||||
|
||||
// ── Slot map helpers ──────────────────────────────────────────────────────────
|
||||
// Slot maps are [String] lists: [key, val, key, val, ...]
|
||||
|
||||
fn slots_get(slots: [String], key: String) -> String {
|
||||
let n: Int = native_list_len(slots)
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(slots, i)
|
||||
if str_eq(k, key) {
|
||||
return native_list_get(slots, i + 1)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fn slots_set(slots: [String], key: String, val: String) -> [String] {
|
||||
let n: Int = native_list_len(slots)
|
||||
let result: [String] = native_list_empty()
|
||||
let found: Bool = false
|
||||
let i: Int = 0
|
||||
while i < n - 1 {
|
||||
let k: String = native_list_get(slots, i)
|
||||
let v: String = native_list_get(slots, i + 1)
|
||||
if str_eq(k, key) {
|
||||
let result = native_list_append(result, k)
|
||||
let result = native_list_append(result, val)
|
||||
let found = true
|
||||
} else {
|
||||
let result = native_list_append(result, k)
|
||||
let result = native_list_append(result, v)
|
||||
}
|
||||
let i = i + 2
|
||||
}
|
||||
if !found {
|
||||
let result = native_list_append(result, key)
|
||||
let result = native_list_append(result, val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn make_slots(k0: String, v0: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, k0)
|
||||
let r = native_list_append(r, v0)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots2(k0: String, v0: String, k1: String, v1: String) -> [String] {
|
||||
let r: [String] = make_slots(k0, v0)
|
||||
let r = native_list_append(r, k1)
|
||||
let r = native_list_append(r, v1)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots3(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String) -> [String] {
|
||||
let r: [String] = make_slots2(k0, v0, k1, v1)
|
||||
let r = native_list_append(r, k2)
|
||||
let r = native_list_append(r, v2)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots4(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String) -> [String] {
|
||||
let r: [String] = make_slots3(k0, v0, k1, v1, k2, v2)
|
||||
let r = native_list_append(r, k3)
|
||||
let r = native_list_append(r, v3)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_slots5(k0: String, v0: String, k1: String, v1: String, k2: String, v2: String, k3: String, v3: String, k4: String, v4: String) -> [String] {
|
||||
let r: [String] = make_slots4(k0, v0, k1, v1, k2, v2, k3, v3)
|
||||
let r = native_list_append(r, k4)
|
||||
let r = native_list_append(r, v4)
|
||||
return r
|
||||
}
|
||||
|
||||
// ── Grammar rule catalog ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Rules:
|
||||
// S-DECL S -> NP VP declarative sentence
|
||||
// S-QUEST S -> Aux NP VP yes/no question
|
||||
// S-IMP S -> VP imperative
|
||||
// NP-DET-N NP -> Det N the cat
|
||||
// NP-DET-ADJ-N NP -> Det Adj N the big cat
|
||||
// NP-PRON NP -> Pron she/he/they
|
||||
// NP-N NP -> N proper noun / bare noun
|
||||
// VP-V VP -> V intransitive
|
||||
// VP-V-NP VP -> V NP transitive
|
||||
// VP-V-PP VP -> V PP locative
|
||||
// VP-V-NP-PP VP -> V NP PP ditransitive+pp
|
||||
// VP-AUX-V VP -> Aux V modal
|
||||
// VP-AUX-V-NP VP -> Aux V NP modal transitive
|
||||
// PP-P-NP PP -> P NP prepositional phrase
|
||||
|
||||
fn rule_id(rule: [String]) -> String {
|
||||
return native_list_get(rule, 0)
|
||||
}
|
||||
|
||||
fn rule_lhs(rule: [String]) -> String {
|
||||
return native_list_get(rule, 1)
|
||||
}
|
||||
|
||||
fn rule_rhs_len(rule: [String]) -> Int {
|
||||
let n: Int = native_list_len(rule)
|
||||
return n - 2
|
||||
}
|
||||
|
||||
fn rule_rhs(rule: [String], idx: Int) -> String {
|
||||
return native_list_get(rule, idx + 2)
|
||||
}
|
||||
|
||||
fn make_rule(id: String, lhs: String, r0: String) -> [String] {
|
||||
let r: [String] = native_list_empty()
|
||||
let r = native_list_append(r, id)
|
||||
let r = native_list_append(r, lhs)
|
||||
let r = native_list_append(r, r0)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_rule2(id: String, lhs: String, r0: String, r1: String) -> [String] {
|
||||
let r: [String] = make_rule(id, lhs, r0)
|
||||
let r = native_list_append(r, r1)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_rule3(id: String, lhs: String, r0: String, r1: String, r2: String) -> [String] {
|
||||
let r: [String] = make_rule2(id, lhs, r0, r1)
|
||||
let r = native_list_append(r, r2)
|
||||
return r
|
||||
}
|
||||
|
||||
fn make_rule4(id: String, lhs: String, r0: String, r1: String, r2: String, r3: String) -> [String] {
|
||||
let r: [String] = make_rule3(id, lhs, r0, r1, r2)
|
||||
let r = native_list_append(r, r3)
|
||||
return r
|
||||
}
|
||||
|
||||
fn build_rules() -> [[String]] {
|
||||
let rules: [[String]] = native_list_empty()
|
||||
|
||||
// Sentence rules
|
||||
let rules = native_list_append(rules, make_rule2("S-DECL", "S", "NP", "VP"))
|
||||
let rules = native_list_append(rules, make_rule3("S-QUEST", "S", "Aux", "NP", "VP"))
|
||||
let rules = native_list_append(rules, make_rule("S-IMP", "S", "VP"))
|
||||
|
||||
// NP rules
|
||||
let rules = native_list_append(rules, make_rule2("NP-DET-N", "NP", "Det", "N"))
|
||||
let rules = native_list_append(rules, make_rule3("NP-DET-ADJ-N","NP","Det", "Adj", "N"))
|
||||
let rules = native_list_append(rules, make_rule("NP-PRON", "NP", "Pron"))
|
||||
let rules = native_list_append(rules, make_rule("NP-N", "NP", "N"))
|
||||
|
||||
// VP rules
|
||||
let rules = native_list_append(rules, make_rule("VP-V", "VP", "V"))
|
||||
let rules = native_list_append(rules, make_rule2("VP-V-NP", "VP", "V", "NP"))
|
||||
let rules = native_list_append(rules, make_rule2("VP-V-PP", "VP", "V", "PP"))
|
||||
let rules = native_list_append(rules, make_rule3("VP-V-NP-PP", "VP", "V", "NP", "PP"))
|
||||
let rules = native_list_append(rules, make_rule2("VP-AUX-V", "VP", "Aux", "V"))
|
||||
let rules = native_list_append(rules, make_rule3("VP-AUX-V-NP","VP", "Aux", "V", "NP"))
|
||||
|
||||
// PP rules
|
||||
let rules = native_list_append(rules, make_rule2("PP-P-NP", "PP", "P", "NP"))
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
fn get_rules() -> [[String]] {
|
||||
return build_rules()
|
||||
}
|
||||
|
||||
fn find_rule(rule_id_str: String) -> [String] {
|
||||
let rules: [[String]] = get_rules()
|
||||
let n: Int = native_list_len(rules)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let rule: [String] = native_list_get(rules, i)
|
||||
let id: String = native_list_get(rule, 0)
|
||||
if str_eq(id, rule_id_str) {
|
||||
return rule
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
let empty: [String] = native_list_empty()
|
||||
return empty
|
||||
}
|
||||
|
||||
// ── Tree node construction ────────────────────────────────────────────────────
|
||||
// A tree node is a [String]: [label, word, num_children, c0_size, c0..., c1_size, c1..., ...]
|
||||
// Since El lists only hold one element type, we serialize tree nodes as
|
||||
// flattened string lists using a simple s-expression encoding.
|
||||
//
|
||||
// Format: "(LABEL WORD CHILD1 CHILD2 ...)"
|
||||
// Leaf node: "(LABEL WORD)"
|
||||
// Non-terminal: "(LABEL _ CHILD1 CHILD2)"
|
||||
|
||||
fn make_leaf(label: String, word: String) -> String {
|
||||
return "(" + label + " " + word + ")"
|
||||
}
|
||||
|
||||
fn make_node1(label: String, child0: String) -> String {
|
||||
return "(" + label + " _ " + child0 + ")"
|
||||
}
|
||||
|
||||
fn make_node2(label: String, child0: String, child1: String) -> String {
|
||||
return "(" + label + " _ " + child0 + " " + child1 + ")"
|
||||
}
|
||||
|
||||
fn make_node3(label: String, child0: String, child1: String, child2: String) -> String {
|
||||
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + ")"
|
||||
}
|
||||
|
||||
fn make_node4(label: String, child0: String, child1: String, child2: String, child3: String) -> String {
|
||||
return "(" + label + " _ " + child0 + " " + child1 + " " + child2 + " " + child3 + ")"
|
||||
}
|
||||
|
||||
// ── Tree rendering: extract the terminal words in order ───────────────────────
|
||||
//
|
||||
// Walk the s-expression and collect all leaf words.
|
||||
|
||||
fn nlg_is_ws(c: String) -> Bool {
|
||||
if str_eq(c, " ") { return true }
|
||||
if str_eq(c, "\t") { return true }
|
||||
if str_eq(c, "\n") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
// Scan forward past whitespace; return new position.
|
||||
fn skip_ws(s: String, pos: Int) -> Int {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = pos
|
||||
let running: Bool = true
|
||||
while running {
|
||||
if i >= n {
|
||||
let running = false
|
||||
} else {
|
||||
let c: String = str_slice(s, i, i + 1)
|
||||
if nlg_is_ws(c) {
|
||||
let i = i + 1
|
||||
} else {
|
||||
let running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Scan a token (non-whitespace, non-paren run); return [token_string, end_pos].
|
||||
fn scan_token(s: String, start: Int) -> [String] {
|
||||
let n: Int = str_len(s)
|
||||
let i: Int = start
|
||||
let running: Bool = true
|
||||
while running {
|
||||
if i >= n {
|
||||
let running = false
|
||||
} else {
|
||||
let c: String = str_slice(s, i, i + 1)
|
||||
if nlg_is_ws(c) {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(c, "(") {
|
||||
let running = false
|
||||
} else {
|
||||
if str_eq(c, ")") {
|
||||
let running = false
|
||||
} else {
|
||||
let i = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let tok: String = str_slice(s, start, i)
|
||||
let result: [String] = native_list_empty()
|
||||
let result = native_list_append(result, tok)
|
||||
let result = native_list_append(result, int_to_str(i))
|
||||
return result
|
||||
}
|
||||
|
||||
// Collect terminal words from a tree s-expression.
|
||||
// Words are the second token in each "(LABEL WORD)" pair where WORD != "_".
|
||||
// render a tree to a flat string by collecting leaf words.
|
||||
// We walk the s-expression character by character.
|
||||
|
||||
fn render_tree(tree: String) -> String {
|
||||
let words: [String] = native_list_empty()
|
||||
let n: Int = str_len(tree)
|
||||
let i: Int = 0
|
||||
// Track depth: after opening paren, the first non-_ token at depth 1
|
||||
// that is followed by a closing paren (or more tokens) is a leaf word.
|
||||
// Strategy: extract all tokens, skip labels (first after '(') and '_'.
|
||||
// All other tokens that aren't '(' or ')' are leaf words.
|
||||
let prev_was_open: Bool = false
|
||||
let is_first_after_open: Bool = false
|
||||
while i < n {
|
||||
let c: String = str_slice(tree, i, i + 1)
|
||||
if str_eq(c, "(") {
|
||||
let prev_was_open = true
|
||||
let i = i + 1
|
||||
} else {
|
||||
if str_eq(c, ")") {
|
||||
let prev_was_open = false
|
||||
let i = i + 1
|
||||
} else {
|
||||
if nlg_is_ws(c) {
|
||||
let i = i + 1
|
||||
} else {
|
||||
// Start of a token
|
||||
let tok_info: [String] = scan_token(tree, i)
|
||||
let tok: String = native_list_get(tok_info, 0)
|
||||
let new_i: Int = str_to_int(native_list_get(tok_info, 1))
|
||||
let i = new_i
|
||||
// If this is the first token after '(' it is a label - skip
|
||||
if prev_was_open {
|
||||
let prev_was_open = false
|
||||
// skip label
|
||||
} else {
|
||||
// It's a word or '_' placeholder
|
||||
if !str_eq(tok, "_") {
|
||||
let words = native_list_append(words, tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return str_join(words, " ")
|
||||
}
|
||||
|
||||
// ── Tree generator ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// generate_tree(rule_id, slots) -> tree s-expression string
|
||||
// slots: a [String] list of [key, val, key, val, ...] pairs
|
||||
//
|
||||
// Known slot keys:
|
||||
// "agent" - NP subject (pronoun or noun phrase string)
|
||||
// "predicate" - verb base form
|
||||
// "patient" - NP object (noun phrase string, optional)
|
||||
// "location" - PP location (e.g. "in the park")
|
||||
// "tense" - "present" | "past" | "future"
|
||||
// "aspect" - "simple" | "progressive" | "perfect"
|
||||
// "det" - determiner for subject NP
|
||||
// "aux" - auxiliary for questions
|
||||
// "verb_surf" - pre-conjugated verb surface form
|
||||
// "aux_surf" - pre-conjugated auxiliary surface form
|
||||
|
||||
fn generate_tree(rule_id_str: String, slots: [String]) -> String {
|
||||
let rule: [String] = find_rule(rule_id_str)
|
||||
let n: Int = native_list_len(rule)
|
||||
if n == 0 {
|
||||
return make_leaf("ERR", "unknown-rule")
|
||||
}
|
||||
|
||||
let lhs: String = native_list_get(rule, 1)
|
||||
let rhs_n: Int = n - 2
|
||||
|
||||
// ── S rules ───────────────────────────────────────────────────────────────
|
||||
if str_eq(rule_id_str, "S-DECL") {
|
||||
let agent: String = slots_get(slots, "agent")
|
||||
let np_tree: String = build_np(agent, slots)
|
||||
let vp_tree: String = build_vp_from_slots(slots)
|
||||
return make_node2("S", np_tree, vp_tree)
|
||||
}
|
||||
|
||||
if str_eq(rule_id_str, "S-QUEST") {
|
||||
let agent: String = slots_get(slots, "agent")
|
||||
let np_tree: String = build_np(agent, slots)
|
||||
let vp_tree: String = build_vp_body(slots)
|
||||
let aux_surf: String = slots_get(slots, "aux_surf")
|
||||
return make_node3("S", make_leaf("Aux", aux_surf), np_tree, vp_tree)
|
||||
}
|
||||
|
||||
if str_eq(rule_id_str, "S-IMP") {
|
||||
let vp_tree: String = build_vp_from_slots(slots)
|
||||
return make_node1("S", vp_tree)
|
||||
}
|
||||
|
||||
return make_leaf(lhs, "?")
|
||||
}
|
||||
|
||||
// Build an NP tree from a referent string.
|
||||
// If the referent is a pronoun (I, you, he, she, it, we, they, me, him, her, us, them),
|
||||
// use NP-PRON. If it looks like "the X" or "a X", parse accordingly.
|
||||
// Otherwise treat as a proper noun.
|
||||
|
||||
fn is_pronoun(word: String) -> Bool {
|
||||
if str_eq(word, "I") { return true }
|
||||
if str_eq(word, "you") { return true }
|
||||
if str_eq(word, "he") { return true }
|
||||
if str_eq(word, "she") { return true }
|
||||
if str_eq(word, "it") { return true }
|
||||
if str_eq(word, "we") { return true }
|
||||
if str_eq(word, "they") { return true }
|
||||
if str_eq(word, "me") { return true }
|
||||
if str_eq(word, "him") { return true }
|
||||
if str_eq(word, "her") { return true }
|
||||
if str_eq(word, "us") { return true }
|
||||
if str_eq(word, "them") { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fn build_np(referent: String, slots: [String]) -> String {
|
||||
if is_pronoun(referent) {
|
||||
return make_node1("NP", make_leaf("Pron", referent))
|
||||
}
|
||||
// Try to parse "DET NOUN" or "DET ADJ NOUN" from the referent string
|
||||
let parts: [String] = str_split(referent, " ")
|
||||
let np: Int = native_list_len(parts)
|
||||
if np == 1 {
|
||||
// Single word - proper noun or bare noun
|
||||
return make_node1("NP", make_leaf("N", referent))
|
||||
}
|
||||
if np == 2 {
|
||||
// DET NOUN
|
||||
let det: String = native_list_get(parts, 0)
|
||||
let noun: String = native_list_get(parts, 1)
|
||||
return make_node2("NP", make_leaf("Det", det), make_leaf("N", noun))
|
||||
}
|
||||
if np == 3 {
|
||||
// DET ADJ NOUN
|
||||
let det: String = native_list_get(parts, 0)
|
||||
let adj: String = native_list_get(parts, 1)
|
||||
let noun: String = native_list_get(parts, 2)
|
||||
return make_node3("NP", make_leaf("Det", det), make_leaf("Adj", adj), make_leaf("N", noun))
|
||||
}
|
||||
// Fallback: treat the whole thing as a name
|
||||
return make_node1("NP", make_leaf("N", referent))
|
||||
}
|
||||
|
||||
fn build_pp(loc: String) -> String {
|
||||
// loc is expected as "PREP NP" e.g. "in the park"
|
||||
let parts: [String] = str_split(loc, " ")
|
||||
let n: Int = native_list_len(parts)
|
||||
if n < 2 {
|
||||
return make_leaf("PP", loc)
|
||||
}
|
||||
let prep: String = native_list_get(parts, 0)
|
||||
// Rest is the NP
|
||||
let np_parts: [String] = native_list_empty()
|
||||
let i: Int = 1
|
||||
while i < n {
|
||||
let np_parts = native_list_append(np_parts, native_list_get(parts, i))
|
||||
let i = i + 1
|
||||
}
|
||||
let np_str: String = str_join(np_parts, " ")
|
||||
let np_tree: String = build_np(np_str, native_list_empty())
|
||||
return make_node2("PP", make_leaf("P", prep), np_tree)
|
||||
}
|
||||
|
||||
fn build_vp_body(slots: [String]) -> String {
|
||||
let verb_surf: String = slots_get(slots, "verb_surf")
|
||||
let patient: String = slots_get(slots, "patient")
|
||||
let loc: String = slots_get(slots, "location")
|
||||
if !str_eq(patient, "") {
|
||||
let obj_np: String = build_np(patient, slots)
|
||||
if !str_eq(loc, "") {
|
||||
let pp: String = build_pp(loc)
|
||||
return make_node3("VP", make_leaf("V", verb_surf), obj_np, pp)
|
||||
}
|
||||
return make_node2("VP", make_leaf("V", verb_surf), obj_np)
|
||||
}
|
||||
if !str_eq(loc, "") {
|
||||
let pp: String = build_pp(loc)
|
||||
return make_node2("VP", make_leaf("V", verb_surf), pp)
|
||||
}
|
||||
return make_node1("VP", make_leaf("V", verb_surf))
|
||||
}
|
||||
|
||||
fn build_vp_from_slots(slots: [String]) -> String {
|
||||
let aux_surf: String = slots_get(slots, "aux_surf")
|
||||
if !str_eq(aux_surf, "") {
|
||||
let verb_surf: String = slots_get(slots, "verb_surf")
|
||||
let patient: String = slots_get(slots, "patient")
|
||||
let loc: String = slots_get(slots, "location")
|
||||
if !str_eq(patient, "") {
|
||||
let obj_np: String = build_np(patient, slots)
|
||||
return make_node3("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf), obj_np)
|
||||
}
|
||||
return make_node2("VP", make_leaf("Aux", aux_surf), make_leaf("V", verb_surf))
|
||||
}
|
||||
return build_vp_body(slots)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
Executable
+112
@@ -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
|
||||
Reference in New Issue
Block a user