Files
el/elp/src/morphology-non.el
T
will.anderson c2cd5e01e1
El SDK CI - dev / build-and-test (pull_request) Successful in 3m34s
fix: elb macOS OpenSSL + C master declarations header; add ELP missing imports
elb.el:
- Auto-detect Homebrew OpenSSL (-L$(brew --prefix openssl)/lib) so -lssl
  resolves on macOS without manual flags; no-op on Linux
- Add -include elp-c-decls.h when present in out_dir: resolves undeclared
  cross-module calls in packages like ELP that lack explicit imports

ELP source:
- Add import "morphology.el" to all 29 language morphology modules
- Add language module imports to morphology.el (all langs it dispatches to)
  These were missing since ELP was originally built as a monolithic unit
2026-05-08 19:44:31 -05:00

557 lines
22 KiB
EmacsLisp

// morphology-non.el - Old Norse morphology for the NLG engine.
//
// Implements Old Norse verb conjugation and noun declension for the ca. 700-1100 CE
// period. Designed as a companion to morphology.el and called by the engine when
// the language profile code is "non".
//
// Language profile: code=non, name=Old Norse, morph_type=fusional, word_order=V2,
// question_strategy=inversion, script=latin, family=germanic.
//
// Verb conjugation covered:
// Tenses: present, past
// Persons: first/second/third x singular/plural (slots 0-5)
// Classes: weak (-a infinitive: -aði past), and a set of common strong/irregular verbs
// Irregulars: vera (be), hafa (have), ganga (go), sjá (see), segja (say),
// koma (come)
// Canonical map: "be" -> "vera"
//
// Noun declension covered:
// Strong masculine a-stem (like "armr")
// Strong feminine ō-stem (like "gör")
// Strong neuter a-stem (like "land")
// Cases: nominative, accusative, genitive, dative
// Numbers: singular, plural
// Definite suffix: appended to the declined form (masc -inn/-ins/-inum, neut -it)
//
// Depends on: morphology.el (str_ends_with, str_len, str_slice, str_eq)
// String helpers
import "morphology.el"
fn non_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn non_drop(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len { return "" }
return str_slice(s, 0, len - n)
}
fn non_last(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 x number to a 0-based paradigm slot.
// 0 = 1st singular (ek)
// 1 = 2nd singular (þú)
// 2 = 3rd singular (hann/hon/þat)
// 3 = 1st plural (vér)
// 4 = 2nd plural (þér)
// 5 = 3rd plural (þeir/þær/þau)
fn non_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
}
// Irregular verb tables
//
// Each irregular verb has a present and past paradigm of six forms (slots 0-5).
fn non_vera_present(slot: Int) -> String {
if slot == 0 { return "em" }
if slot == 1 { return "ert" }
if slot == 2 { return "er" }
if slot == 3 { return "erum" }
if slot == 4 { return "eruð" }
return "eru"
}
fn non_vera_past(slot: Int) -> String {
if slot == 0 { return "var" }
if slot == 1 { return "vart" }
if slot == 2 { return "var" }
if slot == 3 { return "vórum" }
if slot == 4 { return "vóruð" }
return "vóru"
}
fn non_hafa_present(slot: Int) -> String {
if slot == 0 { return "hefi" }
if slot == 1 { return "hefr" }
if slot == 2 { return "hefr" }
if slot == 3 { return "höfum" }
if slot == 4 { return "hafið" }
return "hafa"
}
fn non_hafa_past(slot: Int) -> String {
if slot == 0 { return "hafða" }
if slot == 1 { return "hafðir" }
if slot == 2 { return "hafði" }
if slot == 3 { return "höfðum" }
if slot == 4 { return "höfðuð" }
return "höfðu"
}
fn non_ganga_present(slot: Int) -> String {
if slot == 0 { return "geng" }
if slot == 1 { return "gengr" }
if slot == 2 { return "gengr" }
if slot == 3 { return "göngum" }
if slot == 4 { return "gangið" }
return "ganga"
}
fn non_ganga_past(slot: Int) -> String {
if slot == 0 { return "gekk" }
if slot == 1 { return "gekkt" }
if slot == 2 { return "gekk" }
if slot == 3 { return "gengum" }
if slot == 4 { return "genguð" }
return "gengu"
}
fn non_sja_present(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "sér" }
if slot == 2 { return "sér" }
if slot == 3 { return "séum" }
if slot == 4 { return "séið" }
return "sjá"
}
fn non_sja_past(slot: Int) -> String {
if slot == 0 { return "" }
if slot == 1 { return "sást" }
if slot == 2 { return "" }
if slot == 3 { return "sám" }
if slot == 4 { return "sáð" }
return "sáu"
}
fn non_segja_present(slot: Int) -> String {
if slot == 0 { return "segi" }
if slot == 1 { return "segir" }
if slot == 2 { return "segir" }
if slot == 3 { return "segjum" }
if slot == 4 { return "segið" }
return "segja"
}
fn non_segja_past(slot: Int) -> String {
if slot == 0 { return "sagði" }
if slot == 1 { return "sagðir" }
if slot == 2 { return "sagði" }
if slot == 3 { return "sögðum" }
if slot == 4 { return "sögðuð" }
return "sögðu"
}
fn non_koma_present(slot: Int) -> String {
if slot == 0 { return "kem" }
if slot == 1 { return "kemr" }
if slot == 2 { return "kemr" }
if slot == 3 { return "komum" }
if slot == 4 { return "komið" }
return "koma"
}
fn non_koma_past(slot: Int) -> String {
if slot == 0 { return "kom" }
if slot == 1 { return "komt" }
if slot == 2 { return "kom" }
if slot == 3 { return "komum" }
if slot == 4 { return "komuð" }
return "komu"
}
// Canonical verb mapping
//
// Maps English semantic labels to Old Norse infinitives so the semantic layer
// can request forms without knowing the target language lexeme.
fn non_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "vera" }
if str_eq(verb, "have") { return "hafa" }
if str_eq(verb, "go") { return "ganga" }
if str_eq(verb, "see") { return "sjá" }
if str_eq(verb, "say") { return "segja" }
if str_eq(verb, "come") { return "koma" }
return verb
}
// Weak verb conjugation
//
// Weak verbs (infinitive ending in -a) form the productive conjugation class.
//
// Present tense (stem = drop final -a from infinitive):
// Sg: 1st stem + -a 2nd stem + -ar 3rd stem + -ar
// Pl: 1st stem + -um 2nd stem + -ið 3rd stem + -a
//
// Past tense (suffix -aði- inserted after stem):
// Sg: 1st stem + -aði 2nd stem + -aðir 3rd stem + -aði
// Pl: 1st stem + -uðum 2nd stem + -uðuð 3rd stem + -uðu
fn non_weak_present(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "a" }
if slot == 1 { return stem + "ar" }
if slot == 2 { return stem + "ar" }
if slot == 3 { return stem + "um" }
if slot == 4 { return stem + "" }
return stem + "a"
}
fn non_weak_past(stem: String, slot: Int) -> String {
if slot == 0 { return stem + "aði" }
if slot == 1 { return stem + "aðir" }
if slot == 2 { return stem + "aði" }
if slot == 3 { return stem + "uðum" }
if slot == 4 { return stem + "uðuð" }
return stem + "uðu"
}
// non_conjugate: main conjugation entry point
//
// verb: Old Norse infinitive (e.g. "kalla", "vera") or English canonical label
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
//
// Returns the inflected form. Unknown tenses fall back to the infinitive rather
// than crashing.
fn non_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let v: String = non_map_canonical(verb)
let slot: Int = non_slot(person, number)
// Irregulars
if str_eq(v, "vera") {
if str_eq(tense, "present") { return non_vera_present(slot) }
if str_eq(tense, "past") { return non_vera_past(slot) }
return v
}
if str_eq(v, "hafa") {
if str_eq(tense, "present") { return non_hafa_present(slot) }
if str_eq(tense, "past") { return non_hafa_past(slot) }
return v
}
if str_eq(v, "ganga") {
if str_eq(tense, "present") { return non_ganga_present(slot) }
if str_eq(tense, "past") { return non_ganga_past(slot) }
return v
}
if str_eq(v, "sjá") {
if str_eq(tense, "present") { return non_sja_present(slot) }
if str_eq(tense, "past") { return non_sja_past(slot) }
return v
}
if str_eq(v, "segja") {
if str_eq(tense, "present") { return non_segja_present(slot) }
if str_eq(tense, "past") { return non_segja_past(slot) }
return v
}
if str_eq(v, "koma") {
if str_eq(tense, "present") { return non_koma_present(slot) }
if str_eq(tense, "past") { return non_koma_past(slot) }
return v
}
// Regular weak verb (-a infinitive)
//
// If the verb ends in -a, strip it to get the stem and apply weak endings.
// Otherwise fall back to returning the base form unchanged.
if non_str_ends(v, "a") {
let stem: String = non_drop(v, 1)
if str_eq(tense, "present") { return non_weak_present(stem, slot) }
if str_eq(tense, "past") { return non_weak_past(stem, slot) }
return v
}
// Unknown verb form: return the infinitive unchanged
return v
}
// Strong masculine a-stem declension ("armr" arm)
//
// The paradigm below uses "armr" as the exemplar. The caller passes the full
// nominative singular (including the -r ending); the oblique stem is derived
// by stripping -r (or -ur) from the nominative.
//
// Singular: nom armr acc arm gen arms dat armi
// Plural: nom armar acc arma gen arma dat örmum
//
// The dative plural -örmum involves u-umlaut of the root vowel. Because umlaut
// depends on the root vowel in complex ways, the module stores the dative plural
// stem separately: for "armr" it is "örm". For unknown nouns we approximatehere
// by returning the nominative plural (armar) rather than crashing.
fn non_decline_masc(noun: String, gram_case: String, number: String) -> String {
// Derive oblique stem by dropping the nominative -r ending if present.
let stem: String = noun
if non_str_ends(noun, "r") {
let stem = non_drop(noun, 1)
}
// Hard-code the known exemplar "armr" fully, including the umlauted dat pl.
if str_eq(noun, "armr") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "armr" }
if str_eq(gram_case, "accusative") { return "arm" }
if str_eq(gram_case, "genitive") { return "arms" }
if str_eq(gram_case, "dative") { return "armi" }
return "armr"
}
if str_eq(gram_case, "nominative") { return "armar" }
if str_eq(gram_case, "accusative") { return "arma" }
if str_eq(gram_case, "genitive") { return "arma" }
if str_eq(gram_case, "dative") { return "örmum" }
return "armar"
}
// Generic strong masculine a-stem: use stripped stem + endings.
// Dative plural umlaut is approximated as nominative plural (safe fallback).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return stem + "r" }
if str_eq(gram_case, "accusative") { return stem }
if str_eq(gram_case, "genitive") { return stem + "s" }
if str_eq(gram_case, "dative") { return stem + "i" }
return stem + "r"
}
if str_eq(gram_case, "nominative") { return stem + "ar" }
if str_eq(gram_case, "accusative") { return stem + "a" }
if str_eq(gram_case, "genitive") { return stem + "a" }
if str_eq(gram_case, "dative") { return stem + "um" }
return stem + "ar"
}
// Strong feminine ō-stem declension ("gör" gear)
//
// ō-stem feminines have a distinct set of endings. The stem is the nominative
// singular form itself (no case suffix in nom sg for this class, though some
// nouns add -ar in nom/acc pl).
//
// Singular: nom gör acc görvar gen görvar dat görvi
// Plural: nom görvar acc görvar gen görva dat görvum
fn non_decline_fem(noun: String, gram_case: String, number: String) -> String {
// Use "gör" as the fully specified exemplar.
if str_eq(noun, "gör") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "gör" }
if str_eq(gram_case, "accusative") { return "görvar" }
if str_eq(gram_case, "genitive") { return "görvar" }
if str_eq(gram_case, "dative") { return "görvi" }
return "gör"
}
if str_eq(gram_case, "nominative") { return "görvar" }
if str_eq(gram_case, "accusative") { return "görvar" }
if str_eq(gram_case, "genitive") { return "görva" }
if str_eq(gram_case, "dative") { return "görvum" }
return "görvar"
}
// Generic ō-stem feminine: noun base + endings (approximate).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun + "var" }
if str_eq(gram_case, "genitive") { return noun + "var" }
if str_eq(gram_case, "dative") { return noun + "vi" }
return noun
}
if str_eq(gram_case, "nominative") { return noun + "var" }
if str_eq(gram_case, "accusative") { return noun + "var" }
if str_eq(gram_case, "genitive") { return noun + "va" }
if str_eq(gram_case, "dative") { return noun + "vum" }
return noun + "var"
}
// Strong neuter a-stem declension ("land" land)
//
// Neuter a-stems show no ending in nom/acc sg and u-umlaut in the dative plural.
//
// Singular: nom land acc land gen lands dat landi
// Plural: nom lönd acc lönd gen landa dat löndum
fn non_decline_neut(noun: String, gram_case: String, number: String) -> String {
if str_eq(noun, "land") {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "land" }
if str_eq(gram_case, "accusative") { return "land" }
if str_eq(gram_case, "genitive") { return "lands" }
if str_eq(gram_case, "dative") { return "landi" }
return "land"
}
if str_eq(gram_case, "nominative") { return "lönd" }
if str_eq(gram_case, "accusative") { return "lönd" }
if str_eq(gram_case, "genitive") { return "landa" }
if str_eq(gram_case, "dative") { return "löndum" }
return "lönd"
}
// Generic strong neuter a-stem (no umlaut approximated falls back to base).
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "s" }
if str_eq(gram_case, "dative") { return noun + "i" }
return noun
}
if str_eq(gram_case, "nominative") { return noun }
if str_eq(gram_case, "accusative") { return noun }
if str_eq(gram_case, "genitive") { return noun + "a" }
if str_eq(gram_case, "dative") { return noun + "um" }
return noun
}
// Gender detection heuristic
//
// Old Norse gender must ideally be supplied by the lexicon. As a heuristic:
// ends in -r (after removing) and root looks consonant-final -> masculine
// ends in no case suffix (just the root vowel) -> try feminine
// monosyllabic root without final r -> neuter
//
// The module uses a simplified approach: known exemplars are routed to their
// specific paradigm; everything else defaults to strong masculine.
fn non_detect_gender(noun: String) -> String {
if str_eq(noun, "land") { return "neuter" }
if str_eq(noun, "gör") { return "feminine" }
// Default: masculine
return "masculine"
}
// non_decline: main declension entry point
//
// noun: nominative singular Old Norse noun (e.g. "armr", "land", "gör")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
//
// Returns the inflected form. Falls back to nominative singular on unknown input.
fn non_decline(noun: String, gram_case: String, number: String) -> String {
let gender: String = non_detect_gender(noun)
if str_eq(gender, "masculine") { return non_decline_masc(noun, gram_case, number) }
if str_eq(gender, "feminine") { return non_decline_fem(noun, gram_case, number) }
if str_eq(gender, "neuter") { return non_decline_neut(noun, gram_case, number) }
// Fallback
return noun
}
// Definite suffix table
//
// Old Norse expresses definiteness via a suffix enclitic attached to the end of
// the inflected noun form (not a separate article). The suffix agrees with gender
// and case.
//
// Masculine singular: nom -inn gen -ins dat -inum acc -inn
// Neuter singular: nom -it gen -ins dat -inu acc -it
// Feminine singular: nom -in gen -innar dat -inni acc -ina
// Plural (all genders): nom/acc -inir (masc), -in (neut), -inar (fem)
// simplified to -in for plural in this module.
fn non_def_suffix_masc(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "inn" }
if str_eq(gram_case, "genitive") { return "ins" }
if str_eq(gram_case, "dative") { return "inum" }
if str_eq(gram_case, "accusative") { return "inn" }
return "inn"
}
// plural
if str_eq(gram_case, "nominative") { return "inir" }
if str_eq(gram_case, "accusative") { return "ina" }
if str_eq(gram_case, "genitive") { return "anna" }
if str_eq(gram_case, "dative") { return "unum" }
return "inir"
}
fn non_def_suffix_neut(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "it" }
if str_eq(gram_case, "genitive") { return "ins" }
if str_eq(gram_case, "dative") { return "inu" }
if str_eq(gram_case, "accusative") { return "it" }
return "it"
}
// plural
if str_eq(gram_case, "nominative") { return "in" }
if str_eq(gram_case, "accusative") { return "in" }
if str_eq(gram_case, "genitive") { return "anna" }
if str_eq(gram_case, "dative") { return "unum" }
return "in"
}
fn non_def_suffix_fem(gram_case: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(gram_case, "nominative") { return "in" }
if str_eq(gram_case, "genitive") { return "innar" }
if str_eq(gram_case, "dative") { return "inni" }
if str_eq(gram_case, "accusative") { return "ina" }
return "in"
}
// plural
if str_eq(gram_case, "nominative") { return "inar" }
if str_eq(gram_case, "accusative") { return "inar" }
if str_eq(gram_case, "genitive") { return "anna" }
if str_eq(gram_case, "dative") { return "innar" }
return "inar"
}
// non_noun_phrase: noun phrase builder
//
// Old Norse definiteness is expressed through a suffix enclitic, not a separate
// article. For indefinite nouns the declined form is returned as-is.
//
// noun: nominative singular Old Norse noun (e.g. "armr", "land")
// gram_case: "nominative" | "accusative" | "genitive" | "dative"
// number: "singular" | "plural"
// definite: "true" | "false" (string comparison)
//
// Returns the complete noun phrase string.
fn non_noun_phrase(noun: String, gram_case: String, number: String, definite: String) -> String {
let base: String = non_decline(noun, gram_case, number)
if !str_eq(definite, "true") { return base }
// Append the appropriate definite suffix.
let gender: String = non_detect_gender(noun)
if str_eq(gender, "masculine") {
return base + non_def_suffix_masc(gram_case, number)
}
if str_eq(gender, "neuter") {
return base + non_def_suffix_neut(gram_case, number)
}
if str_eq(gender, "feminine") {
return base + non_def_suffix_fem(gram_case, number)
}
// Fallback: append generic -inn
return base + "inn"
}