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

451 lines
23 KiB
EmacsLisp

// morphology-ja.el - Japanese morphology: verb conjugation and noun particles.
//
// Japanese is SOV, agglutinative, pro-drop. Morphology is suffix-chain based.
//
// Key facts:
// - Nouns do not inflect; grammatical relations are marked by postpositional
// particles attached after the noun.
// - Verbs inflect for tense, polarity, and politeness.
// - Two main verb groups: Ichidan (vowel-stem, Group 2) and Godan (consonant-
// stem, Group 1), plus a small set of irregular verbs.
// - Politeness levels: plain (dictionary/casual) and polite (masu-form).
// - Questions are formed by appending the sentence-final particle (ka).
//
// Depends on: (no dependencies - standalone morphology module)
// Verb group classification
//
// Returns "ichidan" | "godan" | "irregular"
//
// Irregular verbs are checked first. Ichidan verbs end in -る and have a
// vowel immediately before the る. Everything else is Godan.
//
// Note: this is a heuristic classifier for romanized input. For production use
// with native kana/kanji forms, the dictionary form (辞書形) must be consulted.
import "morphology.el"
fn ja_verb_group(dict_form: String) -> String {
// Irregular verbs (exact match on dictionary form)
if str_eq(dict_form, "する") { return "irregular" }
if str_eq(dict_form, "くる") { return "irregular" }
if str_eq(dict_form, "くる") { return "irregular" }
if str_eq(dict_form, "いる") { return "irregular" }
if str_eq(dict_form, "ある") { return "irregular" }
if str_eq(dict_form, "") { return "irregular" }
// Romanized irregulars
if str_eq(dict_form, "suru") { return "irregular" }
if str_eq(dict_form, "kuru") { return "irregular" }
if str_eq(dict_form, "iru") { return "irregular" }
if str_eq(dict_form, "aru") { return "irregular" }
if str_eq(dict_form, "da") { return "irregular" }
// Ichidan: ends in -る (-ru) and has a vowel before it.
// We test using native kana ending and the romanized -eru / -iru pattern.
if str_ends_with(dict_form, "") {
// Any kana-form ending in that is not in the irregular list is
// heuristically classified as Ichidan when the preceding mora ends in
// a vowel sound. For the romanized path we check -eru and -iru endings.
return "ichidan"
}
if str_ends_with(dict_form, "eru") { return "ichidan" }
if str_ends_with(dict_form, "iru") { return "ichidan" }
return "godan"
}
// Ichidan stem extraction
//
// Strip the final -る (-ru) from an Ichidan dictionary form.
// 食べる 食べ taberu tabe
fn ja_ichidan_stem(dict_form: String) -> String {
if str_ends_with(dict_form, "") {
let n: Int = str_len(dict_form)
// str_slice operates on byte offsets; る is 3 UTF-8 bytes.
// We trust the runtime to handle Unicode correctly via str_drop_last.
return str_drop_last(dict_form, 1)
}
if str_ends_with(dict_form, "ru") {
let n: Int = str_len(dict_form)
return str_slice(dict_form, 0, n - 2)
}
return dict_form
}
// Godan stem changes
//
// Godan verbs mutate their final kana to a different row before attaching a
// suffix. The "row" parameter selects which stem form is needed:
//
// "i" - the i-row (連用形 ren'yōkei): for polite forms, te-form base
// "a" - the a-row (未然形 mizenkei): for negative
// "te" - te/ta-form (special sound changes apply)
//
// Final kana i-row:
// Final kana a-row:
// Te/ta-form:
fn ja_godan_stem_change(dict_form: String, row: String) -> String {
let n: Int = str_len(dict_form)
if n == 0 { return dict_form }
// i-row (polite stem / ren'yōkei)
if str_eq(row, "i") {
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
// Romanized fallbacks
if str_ends_with(dict_form, "ku") { return str_drop_last(dict_form, 2) + "ki" }
if str_ends_with(dict_form, "gu") { return str_drop_last(dict_form, 2) + "gi" }
if str_ends_with(dict_form, "su") { return str_drop_last(dict_form, 2) + "shi" }
if str_ends_with(dict_form, "tsu") { return str_drop_last(dict_form, 3) + "chi" }
if str_ends_with(dict_form, "nu") { return str_drop_last(dict_form, 2) + "ni" }
if str_ends_with(dict_form, "bu") { return str_drop_last(dict_form, 2) + "bi" }
if str_ends_with(dict_form, "mu") { return str_drop_last(dict_form, 2) + "mi" }
if str_ends_with(dict_form, "ru") { return str_drop_last(dict_form, 2) + "ri" }
if str_ends_with(dict_form, "u") { return str_drop_last(dict_form, 1) + "i" }
return dict_form
}
// a-row (negative stem / mizenkei)
if str_eq(row, "a") {
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
// Romanized fallbacks
if str_ends_with(dict_form, "ku") { return str_drop_last(dict_form, 2) + "ka" }
if str_ends_with(dict_form, "gu") { return str_drop_last(dict_form, 2) + "ga" }
if str_ends_with(dict_form, "su") { return str_drop_last(dict_form, 2) + "sa" }
if str_ends_with(dict_form, "tsu") { return str_drop_last(dict_form, 3) + "ta" }
if str_ends_with(dict_form, "nu") { return str_drop_last(dict_form, 2) + "na" }
if str_ends_with(dict_form, "bu") { return str_drop_last(dict_form, 2) + "ba" }
if str_ends_with(dict_form, "mu") { return str_drop_last(dict_form, 2) + "ma" }
if str_ends_with(dict_form, "ru") { return str_drop_last(dict_form, 2) + "ra" }
if str_ends_with(dict_form, "u") { return str_drop_last(dict_form, 1) + "wa" }
return dict_form
}
// te/ta-row (te-form and plain past; special euphonic changes)
// Sound changes: い, (voiced), し, つ/る/うっ, ぬ/ぶ/む
if str_eq(row, "te") {
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
if str_ends_with(dict_form, "") { return str_drop_last(dict_form, 1) + "" }
// Romanized fallbacks
if str_ends_with(dict_form, "ku") { return str_drop_last(dict_form, 2) + "i" }
if str_ends_with(dict_form, "gu") { return str_drop_last(dict_form, 2) + "i" }
if str_ends_with(dict_form, "su") { return str_drop_last(dict_form, 2) + "shi" }
if str_ends_with(dict_form, "tsu") { return str_drop_last(dict_form, 3) + "tt" }
if str_ends_with(dict_form, "nu") { return str_drop_last(dict_form, 2) + "n" }
if str_ends_with(dict_form, "bu") { return str_drop_last(dict_form, 2) + "n" }
if str_ends_with(dict_form, "mu") { return str_drop_last(dict_form, 2) + "n" }
if str_ends_with(dict_form, "ru") { return str_drop_last(dict_form, 2) + "tt" }
if str_ends_with(dict_form, "u") { return str_drop_last(dict_form, 1) + "tt" }
return dict_form
}
return dict_form
}
// Verb conjugation
//
// ja_conjugate(dict_form, form) -> String
//
// form values:
// "present" - plain non-past (dictionary form)
// "past" - plain past
// "negative" - plain negative
// "volitional" - plain volitional (let's )
// "polite" - polite non-past (masu-form)
// "polite-past" - polite past (mashita-form)
// "polite-neg" - polite negative (masen-form)
// "te" - te-form (connective / gerund)
fn ja_conjugate(dict_form: String, form: String) -> String {
let group: String = ja_verb_group(dict_form)
// Irregular verbs
if str_eq(group, "irregular") {
// する / suru (to do)
if str_eq(dict_form, "する") {
if str_eq(form, "present") { return "する" }
if str_eq(form, "past") { return "した" }
if str_eq(form, "negative") { return "しない" }
if str_eq(form, "volitional") { return "しよう" }
if str_eq(form, "polite") { return "します" }
if str_eq(form, "polite-past") { return "しました" }
if str_eq(form, "polite-neg") { return "しません" }
if str_eq(form, "te") { return "して" }
return dict_form
}
if str_eq(dict_form, "suru") {
if str_eq(form, "present") { return "suru" }
if str_eq(form, "past") { return "shita" }
if str_eq(form, "negative") { return "shinai" }
if str_eq(form, "volitional") { return "shiyou" }
if str_eq(form, "polite") { return "shimasu" }
if str_eq(form, "polite-past") { return "shimashita" }
if str_eq(form, "polite-neg") { return "shimasen" }
if str_eq(form, "te") { return "shite" }
return dict_form
}
// くる / kuru (to come)
if str_eq(dict_form, "くる") {
if str_eq(form, "present") { return "くる" }
if str_eq(form, "past") { return "きた" }
if str_eq(form, "negative") { return "こない" }
if str_eq(form, "volitional") { return "こよう" }
if str_eq(form, "polite") { return "きます" }
if str_eq(form, "polite-past") { return "きました" }
if str_eq(form, "polite-neg") { return "きません" }
if str_eq(form, "te") { return "きて" }
return dict_form
}
if str_eq(dict_form, "kuru") {
if str_eq(form, "present") { return "kuru" }
if str_eq(form, "past") { return "kita" }
if str_eq(form, "negative") { return "konai" }
if str_eq(form, "volitional") { return "koyou" }
if str_eq(form, "polite") { return "kimasu" }
if str_eq(form, "polite-past") { return "kimashita" }
if str_eq(form, "polite-neg") { return "kimasen" }
if str_eq(form, "te") { return "kite" }
return dict_form
}
// いる / iru (to be / exist, animate)
if str_eq(dict_form, "いる") {
if str_eq(form, "present") { return "いる" }
if str_eq(form, "past") { return "いた" }
if str_eq(form, "negative") { return "いない" }
if str_eq(form, "volitional") { return "いよう" }
if str_eq(form, "polite") { return "います" }
if str_eq(form, "polite-past") { return "いました" }
if str_eq(form, "polite-neg") { return "いません" }
if str_eq(form, "te") { return "いて" }
return dict_form
}
if str_eq(dict_form, "iru") {
if str_eq(form, "present") { return "iru" }
if str_eq(form, "past") { return "ita" }
if str_eq(form, "negative") { return "inai" }
if str_eq(form, "volitional") { return "iyou" }
if str_eq(form, "polite") { return "imasu" }
if str_eq(form, "polite-past") { return "imashita" }
if str_eq(form, "polite-neg") { return "imasen" }
if str_eq(form, "te") { return "ite" }
return dict_form
}
// ある / aru (to be / exist, inanimate)
if str_eq(dict_form, "ある") {
if str_eq(form, "present") { return "ある" }
if str_eq(form, "past") { return "あった" }
if str_eq(form, "negative") { return "ない" }
if str_eq(form, "volitional") { return "あろう" }
if str_eq(form, "polite") { return "あります" }
if str_eq(form, "polite-past") { return "ありました" }
if str_eq(form, "polite-neg") { return "ありません" }
if str_eq(form, "te") { return "あって" }
return dict_form
}
if str_eq(dict_form, "aru") {
if str_eq(form, "present") { return "aru" }
if str_eq(form, "past") { return "atta" }
if str_eq(form, "negative") { return "nai" }
if str_eq(form, "volitional") { return "arou" }
if str_eq(form, "polite") { return "arimasu" }
if str_eq(form, "polite-past") { return "arimashita" }
if str_eq(form, "polite-neg") { return "arimasen" }
if str_eq(form, "te") { return "atte" }
return dict_form
}
// / da (copula)
if str_eq(dict_form, "") {
if str_eq(form, "present") { return "" }
if str_eq(form, "past") { return "だった" }
if str_eq(form, "negative") { return "ではない" }
if str_eq(form, "volitional") { return "だろう" }
if str_eq(form, "polite") { return "です" }
if str_eq(form, "polite-past") { return "でした" }
if str_eq(form, "polite-neg") { return "ではありません" }
if str_eq(form, "te") { return "" }
return dict_form
}
if str_eq(dict_form, "da") {
if str_eq(form, "present") { return "da" }
if str_eq(form, "past") { return "datta" }
if str_eq(form, "negative") { return "dewanai" }
if str_eq(form, "volitional") { return "darou" }
if str_eq(form, "polite") { return "desu" }
if str_eq(form, "polite-past") { return "deshita" }
if str_eq(form, "polite-neg") { return "dewaarimarsen" }
if str_eq(form, "te") { return "de" }
return dict_form
}
// Unknown irregular fall through to base form
return dict_form
}
// Ichidan verbs
if str_eq(group, "ichidan") {
let stem: String = ja_ichidan_stem(dict_form)
if str_eq(form, "present") { return dict_form }
if str_eq(form, "past") { return stem + "" }
if str_eq(form, "negative") { return stem + "ない" }
if str_eq(form, "volitional") { return stem + "よう" }
if str_eq(form, "polite") { return stem + "ます" }
if str_eq(form, "polite-past") { return stem + "ました" }
if str_eq(form, "polite-neg") { return stem + "ません" }
if str_eq(form, "te") { return stem + "" }
return dict_form
}
// Godan verbs
// Godan plain present: dictionary form unchanged
if str_eq(form, "present") { return dict_form }
// Godan polite forms use the i-row stem + masu endings
if str_eq(form, "polite") {
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ます"
}
if str_eq(form, "polite-past") {
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ました"
}
if str_eq(form, "polite-neg") {
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ません"
}
// Godan plain negative uses the a-row stem + nai
if str_eq(form, "negative") {
let astem: String = ja_godan_stem_change(dict_form, "a")
return astem + "ない"
}
// Godan volitional: i-row + ou ( ending おう, others ろう via i-stem)
if str_eq(form, "volitional") {
// う-verbs: drop final and add おう
if str_ends_with(dict_form, "") {
return str_drop_last(dict_form, 1) + "おう"
}
let istem: String = ja_godan_stem_change(dict_form, "i")
return istem + "ろう"
}
// Godan te-form: euphonic te-row stem + (voiced ending いで)
if str_eq(form, "te") {
let tstem: String = ja_godan_stem_change(dict_form, "te")
// Voiced consonants () use instead of
if str_ends_with(dict_form, "") { return tstem + "いで" }
if str_ends_with(dict_form, "gu") { return tstem + "ide" }
// Nasal assimilation: ぬ/ぶ/む んで
if str_ends_with(dict_form, "") { return tstem + "んで" }
if str_ends_with(dict_form, "") { return tstem + "んで" }
if str_ends_with(dict_form, "") { return tstem + "んで" }
if str_ends_with(dict_form, "nu") { return tstem + "nde" }
if str_ends_with(dict_form, "bu") { return tstem + "nde" }
if str_ends_with(dict_form, "mu") { return tstem + "nde" }
// して
if str_ends_with(dict_form, "") { return tstem + "して" }
if str_ends_with(dict_form, "su") { return tstem + "shite" }
// いて (tstem already has )
if str_ends_with(dict_form, "") { return tstem + "" }
if str_ends_with(dict_form, "ku") { return tstem + "te" }
// つ/る/う って
return tstem + ""
}
// Godan plain past: same stem changes as te-form, then た/だ
if str_eq(form, "past") {
let tstem: String = ja_godan_stem_change(dict_form, "te")
if str_ends_with(dict_form, "") { return tstem + "いだ" }
if str_ends_with(dict_form, "gu") { return tstem + "ida" }
if str_ends_with(dict_form, "") { return tstem + "んだ" }
if str_ends_with(dict_form, "") { return tstem + "んだ" }
if str_ends_with(dict_form, "") { return tstem + "んだ" }
if str_ends_with(dict_form, "nu") { return tstem + "nda" }
if str_ends_with(dict_form, "bu") { return tstem + "nda" }
if str_ends_with(dict_form, "mu") { return tstem + "nda" }
if str_ends_with(dict_form, "") { return tstem + "した" }
if str_ends_with(dict_form, "su") { return tstem + "shita" }
if str_ends_with(dict_form, "") { return tstem + "" }
if str_ends_with(dict_form, "ku") { return tstem + "ta" }
return tstem + ""
}
return dict_form
}
// Case particles
//
// Japanese nouns do not inflect; case is indicated by a postpositional particle
// placed directly after the noun.
fn ja_particle(gram_case: String) -> String {
if str_eq(gram_case, "nominative") { return "" }
if str_eq(gram_case, "accusative") { return "" }
if str_eq(gram_case, "dative") { return "" }
if str_eq(gram_case, "genitive") { return "" }
if str_eq(gram_case, "topic") { return "" }
if str_eq(gram_case, "instrumental") { return "" }
if str_eq(gram_case, "locative") { return "" }
if str_eq(gram_case, "ablative") { return "から" }
if str_eq(gram_case, "direction") { return "" }
if str_eq(gram_case, "comitative") { return "" }
return ""
}
// Noun phrase construction
//
// ja_noun_phrase: attach the correct case particle to a noun.
// The particle immediately follows the noun with no space.
fn ja_noun_phrase(noun: String, gram_case: String) -> String {
let p: String = ja_particle(gram_case)
if str_eq(p, "") {
return noun
}
return noun + p
}
// Question particle
//
// Japanese questions are formed by appending (ka) to the end of a sentence.
fn ja_question_particle() -> String {
return ""
}
// Sentence-final particle attachment
//
// ja_make_question: append the question particle to a sentence.
fn ja_make_question(sentence: String) -> String {
return sentence + ja_question_particle()
}