Rename: nlg → elp (Engram Language Protocol)

This commit is contained in:
Will Anderson
2026-05-02 22:15:25 -05:00
parent cbb27b8d87
commit 34725a3988
125 changed files with 129214 additions and 780 deletions
+576
View File
@@ -0,0 +1,576 @@
// morphology-cop.el - Coptic (Sahidic dialect) morphology for the NLG engine.
//
// Implements Coptic verb conjugation (bipartite and tripartite patterns), noun
// phrase assembly with definite and indefinite articles, and noun number marking.
// Designed as a companion to morphology.el; called when language code is "cop".
//
// Language profile: code=cop, name=Coptic, morph_type=agglutinative,
// word_order=SVO, question_strategy=particle, script=coptic, family=afro-asiatic-egyptian.
//
// Script: Coptic uses the Greek alphabet plus seven additional letters borrowed
// from Demotic Egyptian. All Coptic-script characters in this file use their
// correct Unicode code points (Coptic block U+2C80U+2CFF; Coptic letters also
// appear in the Greek block: ϣ U+03E3, ϥ U+03E5, ϩ U+03E9, ϫ U+03EB, ϭ U+03ED).
//
// The El runtime stores strings as byte arrays. String literals with Coptic
// Unicode characters are encoded as UTF-8 and compared via str_eq byte equality.
// The runtime limitation on non-ASCII *output display* does not affect internal
// string logic str_eq and concatenation work correctly.
//
// Grammatical notes (Sahidic Coptic, ca. 2001000 CE):
// - SVO word order (Greek influence; reversed from classical Egyptian)
// - Definite articles prefixed directly to the noun (no space):
// p- (masc sg), t- (fem sg), n- (plural) definite
// ou- (sg indefinite), hen- (pl indefinite)
// - Grammatical gender: masculine / feminine (still active)
// - No case endings grammatical role expressed by word order + prepositions
// - Verb tense/aspect expressed by conjugation base (bipartite pattern):
// Present I: pronoun prefix + verb stem ("f-bwk" = he goes)
// Perfect: a- + pronoun prefix + verb ("a-f-bwk" = he went)
// Future: pronoun prefix + na- + verb ("f-na-bwk" = he will go)
// - Pronoun prefixes (Sahidic used as subject markers in bipartite conjugation):
// 1sg: a-/t- (full: ⲁⲛⲟⲕ) 2sg m: k- 2sg f: te-
// 3sg m: f- 3sg f: s-
// 1pl: n- 2pl: teten- 3pl: se-
// - Copula: "pe" (m sg), "te" (f sg), "ne" (pl); zero copula for adj predicates
// - "to be/become": ϣωπε (Sahidic; present: fϣoop / sϣoop; past: afϣwpe)
//
// Verbs covered (Sahidic transliteration / Coptic script):
// ϣωπε (shwpe) to be / become bwk to go
// nau to see jw to say / speak
// di to give
//
// Canonical English Coptic mapping:
// "be" ϣωπε / zero copula "go" bwk
// "see" nau "say" jw
// "give" di
//
// Persons/numbers covered:
// person: "first" | "second" | "third"
// gender: "m" | "f" (relevant for 2sg and 3sg pronoun prefix selection)
// number: "singular" | "plural"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with)
// String helpers
fn cop_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn cop_str_len(s: String) -> Int {
return str_len(s)
}
fn cop_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn cop_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Person/number slot
//
// Maps person × number to a 0-based index used in paradigm tables.
// Gender is not encoded in the slot index here; it is passed separately to
// cop_subject_prefix where it matters (2sg and 3sg distinction).
//
// Slot layout:
// 0 = 1st singular (ⲁⲛⲟⲕ anok)
// 1 = 2nd singular (ⲛⲧⲟⲕ/ⲛⲧⲟ ntok/nto) gender resolved in cop_subject_prefix
// 2 = 3rd singular (ⲛⲧⲟϥ/ⲛⲧⲟⲥ ntof/ntos) gender resolved in cop_subject_prefix
// 3 = 1st plural (ⲁⲛⲟⲛ anon)
// 4 = 2nd plural (ⲛⲧⲱⲧⲉⲛ ntwten)
// 5 = 3rd plural (ⲛⲧⲟⲩ ntou)
fn cop_slot(person: String, number: String) -> Int {
if str_eq(person, "first") {
if str_eq(number, "singular") { return 0 }
return 3
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return 1 }
return 4
}
// third
if str_eq(number, "singular") { return 2 }
return 5
}
// Subject pronoun prefixes
//
// Coptic bipartite conjugation uses short pronoun prefixes attached directly to
// the verb stem (or to the tense base in tripartite). These are the Sahidic
// bound subject pronouns.
//
// Full independent pronouns (for reference):
// 1sg: ⲁⲛⲟⲕ (anok) prefix: ⲁ- / ⲧ- (varies by tense base)
// 2sg m: ⲛⲧⲟⲕ (ntok) prefix: ⲕ-
// 2sg f: ⲛⲧⲟ (nto) prefix: ⲧⲉ-
// 3sg m: ⲛⲧⲟϥ (ntof) prefix: ϥ-
// 3sg f: ⲛⲧⲟⲥ (ntos) prefix: -
// 1pl: ⲁⲛⲟⲛ (anon) prefix: ⲛ-
// 2pl: ⲛⲧⲱⲧⲉⲛ (ntwten) prefix: ⲧⲉⲧⲉⲛ-
// 3pl: ⲛⲧⲟⲩ (ntou) prefix: ⲥⲉ-
//
// cop_subject_prefix returns the short bound prefix used in bipartite conjugation.
// For the perfect (a-prefix tense base), the subject prefix follows "a-" directly.
fn cop_subject_prefix(person: String, number: String) -> String {
if str_eq(person, "first") {
if str_eq(number, "singular") { return "" }
return ""
}
if str_eq(person, "second") {
if str_eq(number, "singular") { return "" }
return "ⲧⲉⲧⲉⲛ"
}
// third
if str_eq(number, "singular") { return "ϥ" }
return "ⲥⲉ"
}
// cop_subject_prefix_gendered: like cop_subject_prefix but handles the
// 2sg feminine (ⲧⲉ-) and 3sg feminine (-) distinction.
fn cop_subject_prefix_gendered(person: String, gender: String, number: String) -> String {
if str_eq(person, "first") {
if str_eq(number, "singular") { return "" }
return ""
}
if str_eq(person, "second") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return "ⲧⲉ" }
return ""
}
return "ⲧⲉⲧⲉⲛ"
}
// third person
if str_eq(number, "singular") {
if str_eq(gender, "f") { return "" }
return "ϥ"
}
return "ⲥⲉ"
}
// Copula
//
// The Coptic nominal/adjectival copula is a standalone particle that agrees with
// the gender and number of the subject:
// Masculine sg: ⲡⲉ (pe)
// Feminine sg: ⲧⲉ (te)
// Plural: ⲛⲉ (ne)
//
// For adjective predicates in the present tense, the copula is often zero
// (following the inherited Egyptian zero-copula rule). This engine returns ""
// for the present adjective predicate and the full copula particle otherwise.
fn cop_copula_particle(gender: String, number: String) -> String {
if str_eq(number, "plural") { return "ⲛⲉ" }
if str_eq(gender, "f") { return "ⲧⲉ" }
return "ⲡⲉ"
}
// Verb: ϣωπε (to be / become)
//
// ϣωπε is the Sahidic verb meaning "to be" or "to become". It is used as a
// substantive/existential copula. For adjective predicate sentences the zero
// copula is preferred (inherited from Egyptian).
//
// Sahidic forms:
// Present I (bipartite): prefix + ϣⲟⲟⲡ (e.g. ϥϣⲟⲟⲡ "he is/exists")
// Perfect (a- base): + prefix + ϣⲱⲡⲉ (e.g. ⲁϥϣⲱⲡⲉ "he became")
// Future (na- infix): prefix + ⲛⲁϣⲱⲡⲉ (e.g. ϥⲛⲁϣⲱⲡⲉ "he will become")
//
// Note: ϣⲟⲟⲡ (shoop) is the present stem; ϣⲱⲡⲉ (shwpe) is the infinitive/perfect stem.
fn cop_shwpe_present(prefix: String) -> String {
return prefix + "ϣⲟⲟⲡ"
}
fn cop_shwpe_perfect(prefix: String) -> String {
return "" + prefix + "ϣⲱⲡⲉ"
}
fn cop_shwpe_future(prefix: String) -> String {
return prefix + "ⲛⲁϣⲱⲡⲉ"
}
// Verb: bwk (to go) written ⲃⲱⲕ
//
// A common strong verb. The standard bipartite/tripartite pattern applies.
// Present: prefix + ⲃⲱⲕ (e.g. ϥⲃⲱⲕ "he goes")
// Perfect: + prefix + ⲃⲱⲕ (e.g. ⲁϥⲃⲱⲕ "he went")
// Future: prefix + ⲛⲁⲃⲱⲕ (e.g. ϥⲛⲁⲃⲱⲕ "he will go")
fn cop_bwk_present(prefix: String) -> String {
return prefix + "ⲃⲱⲕ"
}
fn cop_bwk_perfect(prefix: String) -> String {
return "" + prefix + "ⲃⲱⲕ"
}
fn cop_bwk_future(prefix: String) -> String {
return prefix + "ⲛⲁⲃⲱⲕ"
}
// Verb: nau (to see) written ⲛⲁⲩ
//
// nau is a biconsonantal verb. Regular bipartite conjugation:
// Present: prefix + ⲛⲁⲩ (e.g. ϥⲛⲁⲩ "he sees")
// Perfect: + prefix + ⲛⲁⲩ (e.g. ⲁϥⲛⲁⲩ "he saw")
// Future: prefix + ⲛⲁⲛⲁⲩ (e.g. ϥⲛⲁⲛⲁⲩ "he will see")
//
// Note: the future prefix "na-" followed by "nau" produces "nanau" standard.
fn cop_nau_present(prefix: String) -> String {
return prefix + "ⲛⲁⲩ"
}
fn cop_nau_perfect(prefix: String) -> String {
return "" + prefix + "ⲛⲁⲩ"
}
fn cop_nau_future(prefix: String) -> String {
return prefix + "ⲛⲁⲛⲁⲩ"
}
// Verb: jw (to say / speak) written ϫⲱ
//
// ϫⲱ is the Sahidic verb for "to say". Bipartite pattern:
// Present: prefix + ϫⲱ (e.g. ϥϫⲱ "he says")
// Perfect: + prefix + ϫⲱ (e.g. ⲁϥϫⲱ "he said")
// Future: prefix + ⲛⲁϫⲱ (e.g. ϥⲛⲁϫⲱ "he will say")
fn cop_jw_present(prefix: String) -> String {
return prefix + "ϫⲱ"
}
fn cop_jw_perfect(prefix: String) -> String {
return "" + prefix + "ϫⲱ"
}
fn cop_jw_future(prefix: String) -> String {
return prefix + "ⲛⲁϫⲱ"
}
// Verb: di (to give) written ϯ
//
// ϯ (ti/di) is a monosyllabic verb meaning "to give". It is very common in
// Coptic texts. Bipartite pattern:
// Present: prefix + ϯ (e.g. ϥϯ "he gives")
// Perfect: + prefix + ϯ (e.g. ⲁϥϯ "he gave")
// Future: prefix + ⲛⲁϯ (e.g. ϥⲛⲁϯ "he will give")
fn cop_di_present(prefix: String) -> String {
return prefix + "ϯ"
}
fn cop_di_perfect(prefix: String) -> String {
return "" + prefix + "ϯ"
}
fn cop_di_future(prefix: String) -> String {
return prefix + "ⲛⲁϯ"
}
// Copula detection
fn cop_is_copula(verb: String) -> Bool {
if str_eq(verb, "ϣωπε") { return true }
if str_eq(verb, "shwpe") { return true }
if str_eq(verb, "be") { return true }
return false
}
// Known-verb dispatcher
//
// Returns the inflected form for a known verb given the subject prefix string
// and tense. Returns "" if the verb is not in the table.
fn cop_known_verb_prefixed(verb: String, tense: String, prefix: String) -> String {
// ϣωπε / shwpe / "be" to be / become
if str_eq(verb, "ϣωπε") {
if str_eq(tense, "present") { return cop_shwpe_present(prefix) }
if str_eq(tense, "past") { return cop_shwpe_perfect(prefix) }
if str_eq(tense, "future") { return cop_shwpe_future(prefix) }
return cop_shwpe_present(prefix)
}
if str_eq(verb, "shwpe") {
if str_eq(tense, "present") { return cop_shwpe_present(prefix) }
if str_eq(tense, "past") { return cop_shwpe_perfect(prefix) }
if str_eq(tense, "future") { return cop_shwpe_future(prefix) }
return cop_shwpe_present(prefix)
}
// bwk / ⲃⲱⲕ to go
if str_eq(verb, "bwk") {
if str_eq(tense, "present") { return cop_bwk_present(prefix) }
if str_eq(tense, "past") { return cop_bwk_perfect(prefix) }
if str_eq(tense, "future") { return cop_bwk_future(prefix) }
return cop_bwk_present(prefix)
}
if str_eq(verb, "ⲃⲱⲕ") {
if str_eq(tense, "present") { return cop_bwk_present(prefix) }
if str_eq(tense, "past") { return cop_bwk_perfect(prefix) }
if str_eq(tense, "future") { return cop_bwk_future(prefix) }
return cop_bwk_present(prefix)
}
if str_eq(verb, "go") {
if str_eq(tense, "present") { return cop_bwk_present(prefix) }
if str_eq(tense, "past") { return cop_bwk_perfect(prefix) }
if str_eq(tense, "future") { return cop_bwk_future(prefix) }
return cop_bwk_present(prefix)
}
// nau / ⲛⲁⲩ to see
if str_eq(verb, "nau") {
if str_eq(tense, "present") { return cop_nau_present(prefix) }
if str_eq(tense, "past") { return cop_nau_perfect(prefix) }
if str_eq(tense, "future") { return cop_nau_future(prefix) }
return cop_nau_present(prefix)
}
if str_eq(verb, "ⲛⲁⲩ") {
if str_eq(tense, "present") { return cop_nau_present(prefix) }
if str_eq(tense, "past") { return cop_nau_perfect(prefix) }
if str_eq(tense, "future") { return cop_nau_future(prefix) }
return cop_nau_present(prefix)
}
if str_eq(verb, "see") {
if str_eq(tense, "present") { return cop_nau_present(prefix) }
if str_eq(tense, "past") { return cop_nau_perfect(prefix) }
if str_eq(tense, "future") { return cop_nau_future(prefix) }
return cop_nau_present(prefix)
}
// jw / ϫⲱ to say / speak
if str_eq(verb, "jw") {
if str_eq(tense, "present") { return cop_jw_present(prefix) }
if str_eq(tense, "past") { return cop_jw_perfect(prefix) }
if str_eq(tense, "future") { return cop_jw_future(prefix) }
return cop_jw_present(prefix)
}
if str_eq(verb, "ϫⲱ") {
if str_eq(tense, "present") { return cop_jw_present(prefix) }
if str_eq(tense, "past") { return cop_jw_perfect(prefix) }
if str_eq(tense, "future") { return cop_jw_future(prefix) }
return cop_jw_present(prefix)
}
if str_eq(verb, "say") {
if str_eq(tense, "present") { return cop_jw_present(prefix) }
if str_eq(tense, "past") { return cop_jw_perfect(prefix) }
if str_eq(tense, "future") { return cop_jw_future(prefix) }
return cop_jw_present(prefix)
}
// di / ϯ to give
if str_eq(verb, "di") {
if str_eq(tense, "present") { return cop_di_present(prefix) }
if str_eq(tense, "past") { return cop_di_perfect(prefix) }
if str_eq(tense, "future") { return cop_di_future(prefix) }
return cop_di_present(prefix)
}
if str_eq(verb, "ϯ") {
if str_eq(tense, "present") { return cop_di_present(prefix) }
if str_eq(tense, "past") { return cop_di_perfect(prefix) }
if str_eq(tense, "future") { return cop_di_future(prefix) }
return cop_di_present(prefix)
}
if str_eq(verb, "give") {
if str_eq(tense, "present") { return cop_di_present(prefix) }
if str_eq(tense, "past") { return cop_di_perfect(prefix) }
if str_eq(tense, "future") { return cop_di_future(prefix) }
return cop_di_present(prefix)
}
// Verb not in table
return ""
}
// Regular verb conjugation
//
// For verbs not in the explicit table, apply the productive bipartite pattern:
// Present: prefix + stem
// Perfect: + prefix + stem
// Future: prefix + ⲛⲁ + stem
fn cop_regular_present(prefix: String, stem: String) -> String {
return prefix + stem
}
fn cop_regular_perfect(prefix: String, stem: String) -> String {
return "" + prefix + stem
}
fn cop_regular_future(prefix: String, stem: String) -> String {
return prefix + "ⲛⲁ" + stem
}
// cop_conjugate: main conjugation entry point
//
// verb: Coptic verb (Sahidic stem, transliterated, or English canonical label)
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the fully conjugated form with subject prefix embedded.
// Zero copula ("") is returned for present "be" (adj predicate context).
// For unknown verbs the regular bipartite pattern is applied as a productive fallback.
fn cop_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let prefix: String = cop_subject_prefix(person, number)
// Handle "be" canonical zero copula in present; ϣωπε otherwise
if str_eq(verb, "be") {
if str_eq(tense, "present") { return "" }
if str_eq(tense, "past") { return cop_shwpe_perfect(prefix) }
if str_eq(tense, "future") { return cop_shwpe_future(prefix) }
return ""
}
// Try the known-verb table
let known: String = cop_known_verb_prefixed(verb, tense, prefix)
if !str_eq(known, "") {
return known
}
// Regular productive bipartite conjugation
if str_eq(tense, "present") { return cop_regular_present(prefix, verb) }
if str_eq(tense, "past") { return cop_regular_perfect(prefix, verb) }
if str_eq(tense, "future") { return cop_regular_future(prefix, verb) }
// Unknown tense: return verb as safe fallback
return verb
}
// Article system
//
// cop_article: return the Coptic article string for the given gender/number/definiteness.
//
// Definite articles (prefixed directly to noun, no space):
// Masculine singular: ⲡ- (p-)
// Feminine singular: ⲧ- (t-)
// Plural (both): ⲛ- (n-)
//
// Indefinite articles:
// Singular (both genders): ⲟⲩ- (ou-)
// Plural: ϩⲉⲛ- (hen-)
//
// gender: "m" | "f"
// number: "singular" | "plural"
// definite: "true" | "false"
//
// Returns the article prefix string (to be concatenated with the noun).
fn cop_article(gender: String, number: String, definite: String) -> String {
if str_eq(definite, "true") {
if str_eq(number, "plural") { return "" }
if str_eq(gender, "f") { return "" }
return ""
}
// Indefinite
if str_eq(number, "plural") { return "ϩⲉⲛ" }
return "ⲟⲩ"
}
// Noun number
//
// cop_decline: return the noun in the appropriate number form.
//
// Coptic nouns have no case endings. Grammatical role is expressed entirely by
// word order and prepositions. The gram_case parameter is accepted for API
// symmetry with other morphology modules but has no effect.
//
// Plural formation:
// Coptic plural morphology is highly irregular (inherited from Egyptian and
// influenced by Greek loanwords). Common patterns:
// - Many nouns show no suffix change plurality is indicated only by the plural article ⲛ-.
// - Some nouns take -ⲟⲟⲩⲉ (-ooue): e.g. ϩⲟ (face) ϩⲟⲟⲩⲉ
// - Greek loanwords often add -ⲟⲥ / -ⲟⲩ in Greek fashion
//
// This function implements:
// - No suffix change (base form) as the productive default the article carries number.
// - Words ending in (a common Coptic nominal ending) may take -ⲟⲟⲩⲉ in the plural;
// this suffix is applied only when the caller explicitly requests plural and the
// noun ends in (productive pattern).
// Vocabulary-layer irregular plurals should be stored in vocabulary-cop.el and
// passed already inflected.
fn cop_decline(noun: String, gram_case: String, number: String) -> String {
if str_eq(number, "singular") { return noun }
// Plural: if noun ends in ⲉ, attempt -ooue suffix (common productive pattern)
if cop_str_ends(noun, "") {
let stem: String = cop_drop(noun, 1)
return stem + "ⲟⲟⲩⲉ"
}
// Default: base form (article carries the plural signal)
return noun
}
// Noun phrase assembly
//
// cop_noun_phrase: build a complete Coptic noun phrase.
//
// noun: base noun (Coptic script or transliteration)
// gram_case: accepted for API symmetry; has no effect (Coptic is caseless)
// number: "singular" | "plural"
// definite: "true" | "false"
//
// The article is prefixed directly to the noun with no intervening space,
// following standard Coptic orthographic convention.
// Gender defaults to masculine when not determinable from context; the caller
// should supply the declined noun already in its correct form if gender-sensitive
// plural forms are needed.
fn cop_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let form: String = cop_decline(noun, gram_case, number)
// Infer gender from number: if plural, gender is moot for the article (always ⲛ-)
// For singular, default to masculine (caller provides gender via article if known)
let art: String = cop_article("m", number, definite)
if str_eq(definite, "true") {
return art + form
}
if str_eq(definite, "false") {
// Indefinite article + noun (no space Coptic convention for proclitic articles)
return art + form
}
return form
}
// cop_noun_phrase_gendered: noun phrase with explicit gender for correct article selection.
//
// gender: "m" | "f"
fn cop_noun_phrase_gendered(noun: String, gram_case: String, number: String, definite: String, gender: String) -> String {
let form: String = cop_decline(noun, gram_case, number)
let art: String = cop_article(gender, number, definite)
if str_eq(definite, "true") {
return art + form
}
if str_eq(definite, "false") {
return art + form
}
return form
}
// Canonical verb mapping
//
// cop_map_canonical: map cross-lingual English canonical verb labels to their
// Sahidic Coptic equivalents before dispatching to cop_conjugate.
fn cop_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "be" }
if str_eq(verb, "go") { return "bwk" }
if str_eq(verb, "see") { return "nau" }
if str_eq(verb, "say") { return "jw" }
if str_eq(verb, "speak") { return "jw" }
if str_eq(verb, "give") { return "di" }
// Unknown: return as-is; cop_conjugate will apply the regular pattern
return verb
}