// 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 "nä" } if str_eq(base, "ksi") { return "ksi" } if str_eq(base, "tta") { return "ttä" } if str_eq(base, "ta") { return "tä" } if str_eq(base, "ja") { return "jä" } 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 "kö" } if str_eq(base, "pa") { return "pä" } if str_eq(base, "va") { return "vä" } if str_eq(base, "ma") { return "mä" } 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, "dä") { 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 "kö" } 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 }