Files
el/elp/src/morphology-fi.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

624 lines
26 KiB
EmacsLisp

// morphology-fi.el - Finnish morphology: noun case inflection and verb conjugation.
//
// Finnish is SOV, agglutinative, 15 grammatical cases, no grammatical gender,
// no articles. Morphology is suffix-chain based.
//
// Key facts:
// - Vowel harmony: suffixes harmonize with the stem's vowel class.
// Back vowels (a, o, u) back-harmony suffixes (-ssa, -sta, -an, etc.)
// Front vowels (ä, ö, y) or neutral-only stems front-harmony (-ssä, -stä, -än, etc.)
// - Consonant gradation: alternation between strong and weak consonant grades.
// Full gradation tables are large; this module implements the most common
// patterns. Irregular/exceptional stems must be supplied in inflected form.
// - Verbs conjugate for person (1/2/3) and number (singular/plural), plus tense
// (present/past) and polarity (affirmative/negative).
// - Question suffix: -ko (back harmony) / -kö (front harmony), appended to verb.
//
// Depends on: (no dependencies - standalone morphology module)
// Vowel harmony
//
// fi_harmony(word) -> "back" | "front"
//
// Scan the word right-to-left for the last unambiguously back (a, o, u) or
// front (ä, ö, y) vowel. Neutral vowels (e, i) do not determine the class.
// If only neutral vowels are found, default to "front" (the conservative choice
// for borrowed words and those without clear back vowels).
import "morphology.el"
fn fi_harmony(word: String) -> String {
let n: Int = str_len(word)
let i: Int = n - 1
while i >= 0 {
let c: String = str_slice(word, i, i + 1)
// Back vowels
if str_eq(c, "a") { return "back" }
if str_eq(c, "o") { return "back" }
if str_eq(c, "u") { return "back" }
// Front vowels (UTF-8; Finnish ä is U+00E4, ö is U+00F6)
// In a byte scan we may land mid-codepoint; we do a substring comparison
// by checking two-byte sequences at position i-1 when c is a lead byte.
if str_eq(c, "ä") { return "front" }
if str_eq(c, "ö") { return "front" }
if str_eq(c, "y") { return "front" }
let i = i - 1
}
// Default: front (covers neutral-only and unknown stems)
return "front"
}
// Suffix harmonization
//
// fi_suffix(base, harmony) -> String
//
// Given a back-harmony base suffix, return the harmonized form.
// Only the a/ä alternation is handled here; all other vowels stay the same.
// E.g. fi_suffix("ssa", "front") -> "ssä"
// fi_suffix("ssa", "back") -> "ssa"
fn fi_suffix(base: String, harmony: String) -> String {
if str_eq(harmony, "front") {
// Replace every 'a' with ' and every 'o' in suffix with '.
// We handle only the common patterns used in Finnish case suffixes.
if str_eq(base, "a") { return "ä" }
if str_eq(base, "ssa") { return "ssä" }
if str_eq(base, "sta") { return "stä" }
if str_eq(base, "an") { return "än" }
if str_eq(base, "aan") { return "ään" }
if str_eq(base, "lla") { return "llä" }
if str_eq(base, "lta") { return "ltä" }
if str_eq(base, "lle") { return "lle" }
if str_eq(base, "na") { return "" }
if str_eq(base, "ksi") { return "ksi" }
if str_eq(base, "tta") { return "ttä" }
if str_eq(base, "ta") { return "" }
if str_eq(base, "ja") { return "" }
if str_eq(base, "oja") { return "öjä" }
if str_eq(base, "issa") { return "issä" }
if str_eq(base, "ista") { return "istä" }
if str_eq(base, "ihin") { return "ihin" }
if str_eq(base, "illa") { return "illä" }
if str_eq(base, "ilta") { return "iltä" }
if str_eq(base, "ille") { return "ille" }
if str_eq(base, "ina") { return "inä" }
if str_eq(base, "itta") { return "ittä" }
if str_eq(base, "ko") { return "" }
if str_eq(base, "pa") { return "" }
if str_eq(base, "va") { return "" }
if str_eq(base, "ma") { return "" }
if str_eq(base, "han") { return "hän" }
if str_eq(base, "lla") { return "llä" }
// Fall back: return the back-harmony form unchanged
return base
}
// Back harmony: return as-is
return base
}
// Noun case inflection
//
// fi_noun_case(stem, gram_case, number, harmony) -> String
//
// Computes the inflected noun form from the oblique stem, case name, number
// ("singular" | "plural"), and vowel harmony class.
//
// The "stem" is the oblique/genitive stem (e.g. talo for talo, puhu for puhua).
// For most Type-1 nouns the oblique stem = dictionary form minus final vowel.
// For the singular nominative the dictionary form itself is used (passed directly).
//
// Cases and their suffixes (talo back, stem "talo"):
// nominative sg : stem (no suffix) talo
// nominative pl : stem + t talot
// genitive sg : stem + n talon
// genitive pl : stem + jen / jen talojen
// accusative sg : stem + n (= gen sg) talon
// accusative pl : stem + t (= nom pl) talot
// partitive sg : stem + a/ä taloa
// partitive pl : stem + ja/jä taloja
// inessive sg : stem + ssa/ssä talossa
// inessive pl : stem + issa/issä taloissa
// elative sg : stem + sta/stä talosta
// elative pl : stem + ista/istä taloista
// illative sg : stem + vowel + n taloon (long vowel + n)
// illative pl : stem + ihin taloihin
// adessive sg : stem + lla/llä talolla
// adessive pl : stem + illa/illä taloilla
// ablative sg : stem + lta/ltä talolta
// ablative pl : stem + ilta/iltä taloilta
// allative sg : stem + lle talolle
// allative pl : stem + ille taloille
// essive sg : stem + na/nä talona
// essive pl : stem + ina/inä taloina
// translative sg : stem + ksi taloksi
// translative pl : stem + iksi taloiksi
// instructive pl : stem + in taloin (plural only)
// abessive sg : stem + tta/ttä talotta
// abessive pl : stem + itta/ittä taloitta
// comitative pl : stem + ineen taloineen (plural only)
fn fi_noun_case(stem: String, gram_case: String, number: String, harmony: String) -> String {
let sg: Bool = str_eq(number, "singular")
if str_eq(gram_case, "nominative") {
if sg { return stem }
return stem + "t"
}
if str_eq(gram_case, "genitive") {
if sg { return stem + "n" }
return stem + "jen"
}
if str_eq(gram_case, "accusative") {
if sg { return stem + "n" }
return stem + "t"
}
if str_eq(gram_case, "partitive") {
if sg { return stem + fi_suffix("a", harmony) }
return stem + fi_suffix("ja", harmony)
}
if str_eq(gram_case, "inessive") {
if sg { return stem + fi_suffix("ssa", harmony) }
return stem + fi_suffix("issa", harmony)
}
if str_eq(gram_case, "elative") {
if sg { return stem + fi_suffix("sta", harmony) }
return stem + fi_suffix("ista", harmony)
}
if str_eq(gram_case, "illative") {
if sg {
// Singular illative: final vowel of stem is lengthened + n
// e.g. talo taloon, puu puuhun, käsi käteen
// We take the last character of the stem and double it, then add n.
let last: String = fi_str_last_char(stem)
return stem + last + "n"
}
return stem + fi_suffix("ihin", harmony)
}
if str_eq(gram_case, "adessive") {
if sg { return stem + fi_suffix("lla", harmony) }
return stem + fi_suffix("illa", harmony)
}
if str_eq(gram_case, "ablative") {
if sg { return stem + fi_suffix("lta", harmony) }
return stem + fi_suffix("ilta", harmony)
}
if str_eq(gram_case, "allative") {
// Allative is -lle for both numbers (only the stem differs)
if sg { return stem + "lle" }
return stem + "ille"
}
if str_eq(gram_case, "essive") {
if sg { return stem + fi_suffix("na", harmony) }
return stem + fi_suffix("ina", harmony)
}
if str_eq(gram_case, "translative") {
if sg { return stem + "ksi" }
return stem + "iksi"
}
if str_eq(gram_case, "instructive") {
// Instructive is plural only in modern Finnish
return stem + "in"
}
if str_eq(gram_case, "abessive") {
if sg { return stem + fi_suffix("tta", harmony) }
return stem + fi_suffix("itta", harmony)
}
if str_eq(gram_case, "comitative") {
// Comitative is plural only
return stem + "ineen"
}
// Unknown case: return stem unchanged
return stem
}
// Noun helper: str_last_char
//
// Return the last Unicode character of a string.
// Mirrors the helper in morphology.el; redefined here for standalone use.
fn fi_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 { return "" }
return str_slice(s, n - 1, n)
}
// Full noun inflection (convenience wrapper)
//
// fi_apply_case(noun, gram_case, number) -> String
//
// Accepts the nominative singular form (dictionary form), derives the harmony
// class, and produces the requested case form.
//
// For most regular nouns the oblique stem equals the dictionary form. The
// illative singular is handled by appending the last vowel + n.
fn fi_apply_case(noun: String, gram_case: String, number: String) -> String {
let harmony: String = fi_harmony(noun)
// For nominative singular, return the noun as-is.
if str_eq(gram_case, "nominative") {
if str_eq(number, "singular") { return noun }
return noun + "t"
}
// For all other cases, use the noun as the oblique stem.
// (Callers that need consonant-gradated stems must pass the graded stem
// directly via fi_noun_case.)
return fi_noun_case(noun, gram_case, number, harmony)
}
// Verb stem extraction
//
// fi_verb_stem(dict_form) -> String
//
// Strip the infinitive ending to get the present-tense stem.
//
// Type 1 verbs (most common): infinitive ends in -a/-ä, stem = infinitive - a/ä
// puhua puhu, juosta juos (irregular, handled separately)
// Type 2 verbs: end in -da/-dä, stem = infinitive - da/dä
// syödä syö, juoda juo
// Type 3 verbs: end in -la/-lä, -ra/-rä, -na/-nä, -sta/-stä
// tulla tul + l stem "tull" (double consonant)
// Type 4 verbs: end in -ata/-ätä
// tavata tapaa (irregular lengthening, handled as irregular)
//
// For NLG purposes we handle Type 1 and Type 2 as the most frequent.
fn fi_verb_stem(dict_form: String) -> String {
// Type 2: -da/-dä drop 2 characters
if str_ends_with(dict_form, "da") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "") {
return str_drop_last(dict_form, 2)
}
// Type 3: -lla/-llä, -rra, -nna drop last 2 chars (keeps double consonant)
if str_ends_with(dict_form, "lla") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "llä") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "rra") {
return str_drop_last(dict_form, 2)
}
if str_ends_with(dict_form, "nna") {
return str_drop_last(dict_form, 2)
}
// Type 1 (and default): -a/-ä drop 1 character
if str_ends_with(dict_form, "a") {
return str_drop_last(dict_form, 1)
}
if str_ends_with(dict_form, "ä") {
return str_drop_last(dict_form, 1)
}
return dict_form
}
// Irregular verb table
//
// Returns an 18-element list encoding essential irregular paradigm forms, or an
// empty list if the verb is regular.
//
// Slot layout (0-indexed):
// 0 inf infinitive (dictionary form)
// 1 pres_1sg present 1sg
// 2 pres_2sg present 2sg
// 3 pres_3sg present 3sg
// 4 pres_1pl present 1pl
// 5 pres_2pl present 2pl
// 6 pres_3pl present 3pl
// 7 past_1sg past 1sg
// 8 past_2sg past 2sg
// 9 past_3sg past 3sg
// 10 past_1pl past 1pl
// 11 past_2pl past 2pl
// 12 past_3pl past 3pl
// 13 neg_stem negative stem (used with en/et/ei/emme/ette/eivät)
// 14 cond_stem conditional stem (for future use)
// 15 imp_2sg imperative 2sg
// 16 part_pres present participle stem
// 17 part_past past participle
fn fi_irregular_verb(dict_form: String) -> [String] {
let empty: [String] = []
// olla to be (the most irregular Finnish verb)
if str_eq(dict_form, "olla") {
let r: [String] = ["olla", "olen", "olet", "on", "olemme", "olette", "ovat",
"olin", "olit", "oli", "olimme", "olitte", "olivat",
"ole", "olis", "ole", "oleva", "ollut"]
return r
}
// voida can / to be able to
if str_eq(dict_form, "voida") {
let r: [String] = ["voida", "voin", "voit", "voi", "voimme", "voitte", "voivat",
"voin", "voit", "voi", "voimme", "voitte", "voivat",
"voi", "vois", "voi", "voiva", "voinut"]
return r
}
// mennä to go (Type 3 with irregularities)
if str_eq(dict_form, "mennä") {
let r: [String] = ["mennä", "menen", "menet", "menee", "menemme", "menette", "menevät",
"menin", "menit", "meni", "menimme", "menitte", "menivät",
"mene", "menis", "mene", "menevä", "mennyt"]
return r
}
// tulla to come (Type 3)
if str_eq(dict_form, "tulla") {
let r: [String] = ["tulla", "tulen", "tulet", "tulee", "tulemme", "tulette", "tulevat",
"tulin", "tulit", "tuli", "tulimme", "tulitte", "tulivat",
"tule", "tulis", "tule", "tuleva", "tullut"]
return r
}
// tehdä to do / make (Type 2, irregular)
if str_eq(dict_form, "tehdä") {
let r: [String] = ["tehdä", "teen", "teet", "tekee", "teemme", "teette", "tekevät",
"tein", "teit", "teki", "teimme", "teitte", "tekivät",
"tee", "tekis", "tee", "tekevä", "tehnyt"]
return r
}
// nähdä to see (Type 2, irregular)
if str_eq(dict_form, "nähdä") {
let r: [String] = ["nähdä", "näen", "näet", "näkee", "näemme", "näette", "näkevät",
"näin", "näit", "näki", "näimme", "näitte", "näkivät",
"näe", "näkis", "näe", "näkevä", "nähnyt"]
return r
}
// saada to get / to be able to (Type 2)
if str_eq(dict_form, "saada") {
let r: [String] = ["saada", "saan", "saat", "saa", "saamme", "saatte", "saavat",
"sain", "sait", "sai", "saimme", "saitte", "saivat",
"saa", "sais", "saa", "saava", "saanut"]
return r
}
// pitää must / to like (Type 1 with stem change)
if str_eq(dict_form, "pitää") {
let r: [String] = ["pitää", "pidän", "pidät", "pitää", "pidämme", "pidätte", "pitävät",
"pidin", "pidit", "piti", "pidimme", "piditte", "pitivät",
"pidä", "pitäis", "pidä", "pitävä", "pitänyt"]
return r
}
// tietää to know (Type 1 with stem change)
if str_eq(dict_form, "tietää") {
let r: [String] = ["tietää", "tiedän", "tiedät", "tietää", "tiedämme", "tiedätte", "tietävät",
"tiesin", "tiesit", "tiesi", "tiesimme", "tiesitte", "tiesivät",
"tiedä", "tietäis", "tiedä", "tietävä", "tiennyt"]
return r
}
return empty
}
// Present-tense personal endings
//
// Type-1 verb present tense endings (suffix onto stem):
// 1sg: -n 2sg: -t 3sg: stem-final vowel lengthened (no suffix)
// 1pl: -mme 2pl: -tte 3pl: -vat/-vät
//
// The 3sg form doubles the final vowel of the stem (puhu puhuu, tule tulee).
// The 3pl uses the harmony-dependent -vat/-vät suffix.
fn fi_present_ending(stem: String, person: String, number: String, harmony: String) -> String {
if str_eq(number, "singular") {
if str_eq(person, "first") { return stem + "n" }
if str_eq(person, "second") { return stem + "t" }
if str_eq(person, "third") {
// 3sg: lengthen final vowel
let last: String = fi_str_last_char(stem)
return stem + last
}
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return stem + "mme" }
if str_eq(person, "second") { return stem + "tte" }
if str_eq(person, "third") { return stem + fi_suffix("vat", harmony) }
}
return stem
}
// Past-tense forms
//
// Type-1 verbs form the past by inserting -i- between the stem and the personal
// ending. The stem-final vowel may contract before -i-.
//
// puhua: puhu + i puhui + n puhuin (1sg past)
// Common contraction: stem final -a/-ä drops before -i-
// puhua stem puhu puhu + i = puhui (no drop needed, -u not -a)
// tavata contraction gives tavasi (handled as irregular or Type 4)
// For Type-1 verbs with -u/-y final stem the rule is simple concatenation.
fn fi_past_stem(stem: String) -> String {
// If stem ends in a or ä, they may contract. For Type-1 verbs where the
// infinitive is -aa/-ää the stem ends in -a/-ä; before -i- that often
// gives -oi-/-öi- (e.g. puhua: puhu puhui, but sanoa: sano sanoi).
// The heuristic: if the stem already ends in a vowel other than a/ä, just
// append i. If it ends in a/ä, convert to o/ö + i (common pattern).
let last: String = fi_str_last_char(stem)
if str_eq(last, "a") {
return str_drop_last(stem, 1) + "oi"
}
if str_eq(last, "ä") {
return str_drop_last(stem, 1) + "öi"
}
return stem + "i"
}
fn fi_past_ending(stem: String, person: String, number: String, harmony: String) -> String {
let pstem: String = fi_past_stem(stem)
if str_eq(number, "singular") {
if str_eq(person, "first") { return pstem + "n" }
if str_eq(person, "second") { return pstem + "t" }
if str_eq(person, "third") { return str_drop_last(pstem, 1) }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return pstem + "mme" }
if str_eq(person, "second") { return pstem + "tte" }
if str_eq(person, "third") { return pstem + fi_suffix("vat", harmony) }
}
return pstem
}
// Negative forms
//
// Finnish negation: negative auxiliary ei (conjugated for person/number) +
// verb in the connective (negative) stem = infinitive stem without personal ending.
//
// Negative auxiliary conjugation:
// 1sg: en 2sg: et 3sg: ei
// 1pl: emme 2pl: ette 3pl: eivät
//
// The negative stem for most verbs = present stem (the form without any ending).
fn fi_neg_aux(person: String, number: String) -> String {
if str_eq(number, "singular") {
if str_eq(person, "first") { return "en" }
if str_eq(person, "second") { return "et" }
if str_eq(person, "third") { return "ei" }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return "emme" }
if str_eq(person, "second") { return "ette" }
if str_eq(person, "third") { return "eivät" }
}
return "ei"
}
fn fi_negative(verb: String, person: String, number: String) -> String {
let irreg: [String] = fi_irregular_verb(verb)
let aux: String = fi_neg_aux(person, number)
if native_list_len(irreg) > 0 {
let neg_stem: String = native_list_get(irreg, 13)
return aux + " " + neg_stem
}
let stem: String = fi_verb_stem(verb)
return aux + " " + stem
}
// Main conjugation entry point
//
// fi_conjugate(verb, tense, person, number) -> String
//
// tense: "present" | "past"
// person: "first" | "second" | "third"
// number: "singular" | "plural"
fn fi_conjugate(verb: String, tense: String, person: String, number: String) -> String {
let harmony: String = fi_harmony(verb)
// Check irregular table first
let irreg: [String] = fi_irregular_verb(verb)
if native_list_len(irreg) > 0 {
if str_eq(tense, "present") {
if str_eq(number, "singular") {
if str_eq(person, "first") { return native_list_get(irreg, 1) }
if str_eq(person, "second") { return native_list_get(irreg, 2) }
if str_eq(person, "third") { return native_list_get(irreg, 3) }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return native_list_get(irreg, 4) }
if str_eq(person, "second") { return native_list_get(irreg, 5) }
if str_eq(person, "third") { return native_list_get(irreg, 6) }
}
}
if str_eq(tense, "past") {
if str_eq(number, "singular") {
if str_eq(person, "first") { return native_list_get(irreg, 7) }
if str_eq(person, "second") { return native_list_get(irreg, 8) }
if str_eq(person, "third") { return native_list_get(irreg, 9) }
}
if str_eq(number, "plural") {
if str_eq(person, "first") { return native_list_get(irreg, 10) }
if str_eq(person, "second") { return native_list_get(irreg, 11) }
if str_eq(person, "third") { return native_list_get(irreg, 12) }
}
}
}
// Regular verbs
let stem: String = fi_verb_stem(verb)
if str_eq(tense, "present") {
return fi_present_ending(stem, person, number, harmony)
}
if str_eq(tense, "past") {
return fi_past_ending(stem, person, number, harmony)
}
return stem
}
// Question suffix
//
// Finnish questions are formed by appending -ko (back harmony) or -kö (front
// harmony) directly to the verb (or sometimes another focus word).
fn fi_question_suffix(harmony: String) -> String {
if str_eq(harmony, "front") { return "" }
return "ko"
}
// Question formation
//
// fi_make_question: append the appropriate question suffix to a verb form.
fn fi_make_question(verb_form: String, harmony: String) -> String {
return verb_form + fi_question_suffix(harmony)
}
// Convenience: inflect a noun through all 15 cases
//
// Returns a 30-element list: [case_name, sg_form, case_name, pl_form, ...]
// for all 15 cases. Plural-only cases (instructive, comitative) have an
// empty string for the singular slot.
fn fi_full_paradigm(noun: String) -> [String] {
let harmony: String = fi_harmony(noun)
let r: [String] = []
let cases: [String] = ["nominative", "genitive", "accusative", "partitive",
"inessive", "elative", "illative", "adessive",
"ablative", "allative", "essive", "translative",
"instructive", "abessive", "comitative"]
let n: Int = native_list_len(cases)
let i: Int = 0
while i < n {
let c: String = native_list_get(cases, i)
let r = native_list_append(r, c)
// Singular
if str_eq(c, "instructive") {
let r = native_list_append(r, "")
} else {
if str_eq(c, "comitative") {
let r = native_list_append(r, "")
} else {
let r = native_list_append(r, fi_noun_case(noun, c, "singular", harmony))
}
}
// Plural
let r = native_list_append(r, fi_noun_case(noun, c, "plural", harmony))
let i = i + 1
}
return r
}