Files
el/elp/src/morphology-cop.el
T
2026-05-02 22:15:25 -05:00

577 lines
25 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}