Rename: nlg → elp (Engram Language Protocol)

This commit is contained in:
Will Anderson
2026-05-02 22:15:25 -05:00
parent cbb27b8d87
commit 34725a3988
125 changed files with 129214 additions and 780 deletions
+632
View File
@@ -0,0 +1,632 @@
// morphology-he.el - Hebrew morphology for the NLG engine.
// עברית עיצוב מורפולוגי למנוע ייצור שפה טבעית
//
// Implements Modern Hebrew verb conjugation (Pa'al binyan, participle-based
// present tense), noun pluralization, and definite-article prefixing.
//
// Hebrew is a Semitic language with a trilateral root system shared with
// Arabic. Like Arabic, words are built from three-consonant roots by
// applying vowel patterns (mishkalim) around the root.
//
// Modern Hebrew (the target) profile:
// code=he, name=Hebrew, morph_type=semitic, word_order=SVO,
// question_strategy=intonation, script=hebrew, family=semitic
//
// Key grammatical facts:
// - Grammatical gender: masculine (m) / feminine (f)
// - Number: singular / plural (dual exists but is marginal in Modern Hebrew)
// - Verbs: built from 3-letter roots via binyanim (verb patterns)
// Pa'al (פָּעַל) is covered here the most common pattern for everyday verbs
// - Present tense is participle-based (בינוני / binyan) with gender/number
// agreement suffixes on the verb; it doubles as an adjective form
// - Copula: present tense uses zero copula (omit "to be" entirely);
// past tense uses היה/הייתה/היו (haya/hayta/hayu)
// - Definite article: prefix ה (ha-) attached directly to the noun;
// consonant doubling after ה is simplified to just "ha" prefix here
// - Questions: rising intonation (no morphological change); optional
// particle האם (ha'im) at sentence start; this engine uses intonation
// strategy (question mark added by realizer)
// - Script: Hebrew Unicode (right-to-left). The El runtime currently
// outputs non-ASCII as numeric hashes (runtime limitation), but str_eq
// and string literals work correctly internally. When the VM adds UTF-8
// output support, all Hebrew strings will display correctly automatically.
//
// Verbs covered (by infinitive, transliterated and Hebrew):
// "lihyot" / לִהְיוֹת to be (copula, zero-present, haya-past)
// "haya" / הָיָה to be (dictionary alias lihyot)
// "be" English canonical lihyot
// "lir'ot" / לִרְאוֹת to see
// "le'exol" / לֶאֱכוֹל to eat
// "ledaber" / לְדַבֵּר to speak
// "lalechet" / לָלֶכֶת to go
//
// Conventions used throughout:
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
// tense: "present" | "past" | "future"
//
// Depends on: morphology.el (str_eq, str_len, str_slice, str_ends_with,
// str_drop_last, str_concat/+)
// String helpers
fn he_str_ends(s: String, suf: String) -> Bool {
return str_ends_with(s, suf)
}
fn he_str_len(s: String) -> Int {
return str_len(s)
}
fn he_str_drop_last(s: String, n: Int) -> String {
let len: Int = str_len(s)
if n >= len {
return ""
}
return str_slice(s, 0, len - n)
}
fn he_str_last_char(s: String) -> String {
let n: Int = str_len(s)
if n == 0 {
return ""
}
return str_slice(s, n - 1, n)
}
// Slot index
//
// Maps person × gender × number to a 0-based slot for table lookups.
// Modern Hebrew uses 12 paradigm cells (3 persons × 2 genders × 2 numbers),
// but first person is gender-neutral in Modern Hebrew for verbs, so we
// collapse 1s-m and 1s-f to the same slot, and likewise 1p.
//
// Slot layout:
// 0 = 3ms (הוא hu he)
// 1 = 3fs (היא hi she)
// 2 = 2ms (אתה ata you m sg)
// 3 = 2fs (את at you f sg)
// 4 = 1s (אני ani I, gender-neutral in Modern Hebrew)
// 5 = 3mp (הם hem they m pl)
// 6 = 3fp (הן hen they f pl)
// 7 = 2mp (אתם atem you m pl)
// 8 = 2fp (אתן aten you f pl)
// 9 = 1p (אנחנו anakhnu we, gender-neutral)
fn he_slot(person: String, gender: String, number: String) -> Int {
if str_eq(person, "third") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 1 }
return 0
}
// plural
if str_eq(gender, "f") { return 6 }
return 5
}
if str_eq(person, "second") {
if str_eq(number, "singular") {
if str_eq(gender, "f") { return 3 }
return 2
}
// plural
if str_eq(gender, "f") { return 8 }
return 7
}
// first person gender-neutral in Modern Hebrew verb conjugation
if str_eq(number, "plural") { return 9 }
return 4
}
// Present-tense (participle / binyan) agreement suffixes
//
// Modern Hebrew present tense is built from the Pa'al participle stem.
// The participle agrees with the subject in gender and number.
//
// For Pa'al (CoCeC pattern), the present-tense forms are:
// Masc sg: base participle stem (e.g. רוֹאֶה ro'e sees) [no suffix]
// Fem sg: stem + ת (-t / -et) depending on root
// Masc pl: stem + ים (-im) (often with vowel change in the root)
// Fem pl: stem + ות (-ot)
//
// Rather than attempting to derive these from the root (which requires knowing
// the full vowel pattern), we store the four present-tense forms explicitly
// for each covered verb. This is the standard approach for a finite NLG
// system covering a curated verb set.
//
// Present forms are indexed by a 2-bit form code:
// 0 = masc sg (ms)
// 1 = fem sg (fs)
// 2 = masc pl (mp)
// 3 = fem pl (fp)
fn he_present_form_code(slot: Int) -> Int {
// Slots 0, 2, 4, 7 masc sg form (1s and 2ms use same form as 3ms)
if slot == 0 { return 0 } // 3ms
if slot == 1 { return 1 } // 3fs
if slot == 2 { return 0 } // 2ms masc sg
if slot == 3 { return 1 } // 2fs fem sg
if slot == 4 { return 0 } // 1s masc sg (gender-neutral, default masc)
if slot == 5 { return 2 } // 3mp masc pl
if slot == 6 { return 3 } // 3fp fem pl
if slot == 7 { return 2 } // 2mp masc pl
if slot == 8 { return 3 } // 2fp fem pl
return 2 // 1p (slot 9) masc pl (gender-neutral default)
}
// Copula: היה haya to be
//
// Present copula: ZERO (omitted from surface form).
// Past copula: היה haya (3ms), הייתה hayta (3fs/2fs), היו hayu (3pl),
// הייתי hayiti (1s), הייתם hayitem (2mp), הייתן hayiten (2fp),
// היינו hayinu (1p), היית hayita (2ms).
// Future copula: יהיה yihye (3ms), תהיה tihye (3fs/2), אהיה ehye (1s),
// יהיו yihyu (3pl/2pl), נהיה nihye (1p) included for completeness.
fn he_copula_past(slot: Int) -> String {
if slot == 0 { return "היה" } // 3ms haya
if slot == 1 { return "הייתה" } // 3fs hayta
if slot == 2 { return "היית" } // 2ms hayita
if slot == 3 { return "הייתה" } // 2fs hayta (same as 3fs in Modern Hebrew)
if slot == 4 { return "הייתי" } // 1s hayiti
if slot == 5 { return "היו" } // 3mp hayu
if slot == 6 { return "היו" } // 3fp hayu (same as 3mp in Modern Hebrew)
if slot == 7 { return "הייתם" } // 2mp hayitem
if slot == 8 { return "הייתן" } // 2fp hayiten
return "היינו" // 1p hayinu
}
fn he_copula_future(slot: Int) -> String {
if slot == 0 { return "יהיה" } // 3ms yihye
if slot == 1 { return "תהיה" } // 3fs tihye
if slot == 2 { return "תהיה" } // 2ms tihye
if slot == 3 { return "תהיי" } // 2fs tihyi
if slot == 4 { return "אהיה" } // 1s ehye
if slot == 5 { return "יהיו" } // 3mp yihyu
if slot == 6 { return "יהיו" } // 3fp yihyu
if slot == 7 { return "תהיו" } // 2mp tihyu
if slot == 8 { return "תהיו" } // 2fp tihyu
return "נהיה" // 1p nihye
}
// he_is_copula: detect whether the input verb means "to be".
fn he_is_copula(verb: String) -> Bool {
if str_eq(verb, "lihyot") { return true }
if str_eq(verb, "haya") { return true }
if str_eq(verb, "be") { return true }
if str_eq(verb, "היה") { return true }
if str_eq(verb, "לִהְיוֹת") { return true }
return false
}
// he_conjugate_copula: conjugate the copula for the given tense/slot.
fn he_conjugate_copula(tense: String, slot: Int) -> String {
// Present copula: zero in Modern Hebrew
if str_eq(tense, "present") { return "" }
if str_eq(tense, "past") { return he_copula_past(slot) }
if str_eq(tense, "future") { return he_copula_future(slot) }
// Default: zero copula
return ""
}
// Pa'al present-tense forms: verb-by-verb table
//
// Each verb stores four present-tense participle forms:
// [masc sg, fem sg, masc pl, fem pl]
// indexed by he_present_form_code(slot).
//
// Transliterations for reference:
// lir'ot (לִרְאוֹת to see): ro'e / ro'a / ro'im / ro'ot
// le'exol (לֶאֱכוֹל to eat): oxel / oxelet / oxlim / oxlot
// ledaber (לְדַבֵּר to speak): medaber / medaberet / medabrim / medabrot
// lalechet (לָלֶכֶת to go): holech / holechet / holchim / holchot
fn he_present_lir_ot(form: Int) -> String {
if form == 0 { return "רוֹאֶה" } // ro'e masc sg
if form == 1 { return "רוֹאָה" } // ro'a fem sg
if form == 2 { return "רוֹאִים" } // ro'im masc pl
return "רוֹאוֹת" // ro'ot fem pl (form == 3)
}
fn he_present_le_exol(form: Int) -> String {
if form == 0 { return "אוֹכֵל" } // oxel masc sg
if form == 1 { return "אוֹכֶלֶת" } // oxelet fem sg
if form == 2 { return "אוֹכְלִים" } // oxlim masc pl
return "אוֹכְלוֹת" // oxlot fem pl
}
fn he_present_ledaber(form: Int) -> String {
if form == 0 { return "מְדַבֵּר" } // medaber masc sg
if form == 1 { return "מְדַבֶּרֶת" } // medaberet fem sg
if form == 2 { return "מְדַבְּרִים" } // medabrim masc pl
return "מְדַבְּרוֹת" // medabrot fem pl
}
fn he_present_lalechet(form: Int) -> String {
if form == 0 { return "הוֹלֵךְ" } // holech masc sg
if form == 1 { return "הוֹלֶכֶת" } // holechet fem sg
if form == 2 { return "הוֹלְכִים" } // holchim masc pl
return "הוֹלְכוֹת" // holchot fem pl
}
// Pa'al past-tense forms: verb-by-verb table
//
// Past tense in Pa'al uses suffixes on a past stem (the 3ms form is the stem).
// Suffix pattern (slot suffix appended to stem consonants):
// slot 0 (3ms): base (e.g. ראה ra'a)
// slot 1 (3fs): -ta (ראתה ra'ata)
// slot 2 (2ms): -ta (ראית ra'ita)
// slot 3 (2fs): -t (ראית ra'it same spelling as 2ms in Modern Hebrew)
// slot 4 (1s): -ti (ראיתי ra'iti)
// slot 5 (3mp): -u (ראו ra'u)
// slot 6 (3fp): -u (ראו ra'u same as 3mp in Modern Hebrew)
// slot 7 (2mp): -tem (ראיתם ra'item)
// slot 8 (2fp): -ten (ראיתן ra'iten)
// slot 9 (1p): -nu (ראינו ra'inu)
//
// We store full past paradigms for each verb suffix application to the base
// requires knowing each verb's past stem vowel pattern.
fn he_past_lir_ot(slot: Int) -> String {
if slot == 0 { return "רָאָה" } // ra'a 3ms
if slot == 1 { return "רָאֲתָה" } // ra'ata 3fs
if slot == 2 { return "רָאִיתָ" } // ra'ita 2ms
if slot == 3 { return "רָאִית" } // ra'it 2fs
if slot == 4 { return "רָאִיתִי" } // ra'iti 1s
if slot == 5 { return "רָאוּ" } // ra'u 3mp
if slot == 6 { return "רָאוּ" } // ra'u 3fp
if slot == 7 { return "רְאִיתֶם" } // re'item 2mp
if slot == 8 { return "רְאִיתֶן" } // re'iten 2fp
return "רָאִינוּ" // ra'inu 1p
}
fn he_past_le_exol(slot: Int) -> String {
if slot == 0 { return "אָכַל" } // axal 3ms
if slot == 1 { return "אָכְלָה" } // axla 3fs
if slot == 2 { return "אָכַלְתָּ" } // axalta 2ms
if slot == 3 { return "אָכַלְתְּ" } // axalt 2fs
if slot == 4 { return "אָכַלְתִּי" }// axalti 1s
if slot == 5 { return "אָכְלוּ" } // axlu 3mp
if slot == 6 { return "אָכְלוּ" } // axlu 3fp
if slot == 7 { return "אֲכַלְתֶּם" }// axaltem 2mp
if slot == 8 { return "אֲכַלְתֶּן" }// axalten 2fp
return "אָכַלְנוּ" // axalnu 1p
}
fn he_past_ledaber(slot: Int) -> String {
if slot == 0 { return "דִּבֵּר" } // diber 3ms (Pi'el past)
if slot == 1 { return "דִּבְּרָה" } // dibra 3fs
if slot == 2 { return "דִּבַּרְתָּ" }// dibarta 2ms
if slot == 3 { return "דִּבַּרְתְּ" }// dibart 2fs
if slot == 4 { return "דִּבַּרְתִּי" }// diberti 1s
if slot == 5 { return "דִּבְּרוּ" } // dibru 3mp
if slot == 6 { return "דִּבְּרוּ" } // dibru 3fp
if slot == 7 { return "דִּבַּרְתֶּם" }// dibertem 2mp
if slot == 8 { return "דִּבַּרְתֶּן" }// dibertn 2fp
return "דִּבַּרְנוּ" // dibernu 1p
}
fn he_past_lalechet(slot: Int) -> String {
if slot == 0 { return "הָלַךְ" } // halax 3ms
if slot == 1 { return "הָלְכָה" } // halxa 3fs
if slot == 2 { return "הָלַכְתָּ" } // halaxta 2ms
if slot == 3 { return "הָלַכְתְּ" } // halaxt 2fs
if slot == 4 { return "הָלַכְתִּי" }// halaxti 1s
if slot == 5 { return "הָלְכוּ" } // halxu 3mp
if slot == 6 { return "הָלְכוּ" } // halxu 3fp
if slot == 7 { return "הֲלַכְתֶּם" }// halaxtem 2mp
if slot == 8 { return "הֲלַכְתֶּן" }// halaxten 2fp
return "הָלַכְנוּ" // halaxnu 1p
}
// Future-tense forms: verb-by-verb table
//
// Future tense in Pa'al uses prefix + root + suffix (yiqtol pattern).
// We store full paradigms for each covered verb.
fn he_future_lir_ot(slot: Int) -> String {
if slot == 0 { return "יִרְאֶה" } // yir'e 3ms
if slot == 1 { return "תִּרְאֶה" } // tir'e 3fs
if slot == 2 { return "תִּרְאֶה" } // tir'e 2ms
if slot == 3 { return "תִּרְאִי" } // tir'i 2fs
if slot == 4 { return "אֶרְאֶה" } // er'e 1s
if slot == 5 { return "יִרְאוּ" } // yir'u 3mp
if slot == 6 { return "תִּרְאֶינָה" }// tir'ena 3fp
if slot == 7 { return "תִּרְאוּ" } // tir'u 2mp
if slot == 8 { return "תִּרְאֶינָה" }// tir'ena 2fp
return "נִרְאֶה" // nir'e 1p
}
fn he_future_le_exol(slot: Int) -> String {
if slot == 0 { return "יֹאכַל" } // yoxal 3ms
if slot == 1 { return "תֹּאכַל" } // toxal 3fs
if slot == 2 { return "תֹּאכַל" } // toxal 2ms
if slot == 3 { return "תֹּאכְלִי" } // toxli 2fs
if slot == 4 { return "אֹכַל" } // oxal 1s
if slot == 5 { return "יֹאכְלוּ" } // yoxlu 3mp
if slot == 6 { return "תֹּאכַלְנָה" }// toxalna 3fp
if slot == 7 { return "תֹּאכְלוּ" } // toxlu 2mp
if slot == 8 { return "תֹּאכַלְנָה" }// toxalna 2fp
return "נֹאכַל" // noxal 1p
}
fn he_future_ledaber(slot: Int) -> String {
if slot == 0 { return "יְדַבֵּר" } // yedaber 3ms
if slot == 1 { return "תְּדַבֵּר" } // tedaber 3fs
if slot == 2 { return "תְּדַבֵּר" } // tedaber 2ms
if slot == 3 { return "תְּדַבְּרִי" }// tedabri 2fs
if slot == 4 { return "אֲדַבֵּר" } // adaber 1s
if slot == 5 { return "יְדַבְּרוּ" }// yedabru 3mp
if slot == 6 { return "תְּדַבֵּרְנָה" }// tedaberna 3fp
if slot == 7 { return "תְּדַבְּרוּ" }// tedabru 2mp
if slot == 8 { return "תְּדַבֵּרְנָה" }// tedaberna 2fp
return "נְדַבֵּר" // nedaber 1p
}
fn he_future_lalechet(slot: Int) -> String {
if slot == 0 { return "יֵלֵךְ" } // yelex 3ms
if slot == 1 { return "תֵּלֵךְ" } // telex 3fs
if slot == 2 { return "תֵּלֵךְ" } // telex 2ms
if slot == 3 { return "תֵּלְכִי" } // telxi 2fs
if slot == 4 { return "אֵלֵךְ" } // elex 1s
if slot == 5 { return "יֵלְכוּ" } // yelxu 3mp
if slot == 6 { return "תֵּלַכְנָה" } // telaxna 3fp
if slot == 7 { return "תֵּלְכוּ" } // telxu 2mp
if slot == 8 { return "תֵּלַכְנָה" } // telaxna 2fp
return "נֵלֵךְ" // nelex 1p
}
// Known-verb dispatcher
//
// he_known_verb: return the inflected form for a known verb, or "" if the
// verb is not in the lookup table. Accepts both transliterated and Hebrew
// script infinitives.
fn he_known_verb(verb: String, tense: String, slot: Int) -> String {
// lir'ot / לִרְאוֹת to see
if str_eq(verb, "lir'ot") {
if str_eq(tense, "present") { return he_present_lir_ot(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lir_ot(slot) }
if str_eq(tense, "future") { return he_future_lir_ot(slot) }
return he_present_lir_ot(he_present_form_code(slot))
}
if str_eq(verb, "לִרְאוֹת") {
if str_eq(tense, "present") { return he_present_lir_ot(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lir_ot(slot) }
if str_eq(tense, "future") { return he_future_lir_ot(slot) }
return he_present_lir_ot(he_present_form_code(slot))
}
// le'exol / לֶאֱכוֹל to eat
if str_eq(verb, "le'exol") {
if str_eq(tense, "present") { return he_present_le_exol(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_le_exol(slot) }
if str_eq(tense, "future") { return he_future_le_exol(slot) }
return he_present_le_exol(he_present_form_code(slot))
}
if str_eq(verb, "לֶאֱכוֹל") {
if str_eq(tense, "present") { return he_present_le_exol(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_le_exol(slot) }
if str_eq(tense, "future") { return he_future_le_exol(slot) }
return he_present_le_exol(he_present_form_code(slot))
}
// ledaber / לְדַבֵּר to speak
if str_eq(verb, "ledaber") {
if str_eq(tense, "present") { return he_present_ledaber(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_ledaber(slot) }
if str_eq(tense, "future") { return he_future_ledaber(slot) }
return he_present_ledaber(he_present_form_code(slot))
}
if str_eq(verb, "לְדַבֵּר") {
if str_eq(tense, "present") { return he_present_ledaber(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_ledaber(slot) }
if str_eq(tense, "future") { return he_future_ledaber(slot) }
return he_present_ledaber(he_present_form_code(slot))
}
// lalechet / לָלֶכֶת to go
if str_eq(verb, "lalechet") {
if str_eq(tense, "present") { return he_present_lalechet(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lalechet(slot) }
if str_eq(tense, "future") { return he_future_lalechet(slot) }
return he_present_lalechet(he_present_form_code(slot))
}
if str_eq(verb, "לָלֶכֶת") {
if str_eq(tense, "present") { return he_present_lalechet(he_present_form_code(slot)) }
if str_eq(tense, "past") { return he_past_lalechet(slot) }
if str_eq(tense, "future") { return he_future_lalechet(slot) }
return he_present_lalechet(he_present_form_code(slot))
}
// Verb not in table
return ""
}
// Main conjugation entry point
//
// he_conjugate: conjugate a Hebrew verb.
//
// verb: infinitive (transliterated or Hebrew script)
// tense: "present" | "past" | "future"
// person: "first" | "second" | "third"
// gender: "m" | "f"
// number: "singular" | "plural"
//
// Returns:
// - "" for present copula (zero copula caller omits the verb)
// - inflected form for all other cases
// - the infinitive unchanged for unknown verbs (safe fallback)
fn he_conjugate(verb: String, tense: String, person: String, gender: String, number: String) -> String {
let slot: Int = he_slot(person, gender, number)
// Handle copula first
if he_is_copula(verb) {
return he_conjugate_copula(tense, slot)
}
// Try the known-verb table
let known: String = he_known_verb(verb, tense, slot)
if !str_eq(known, "") {
return known
}
// Unknown verb: return the infinitive as a safe placeholder.
// The caller can detect this when the output equals the input.
return verb
}
// Noun pluralization
//
// he_pluralize: form the plural of a Hebrew noun.
//
// Rules (simplified for Modern Hebrew):
// Masculine nouns (and those not ending in -a or -et):
// Add ים- (-im)
// Feminine nouns ending in -a (transliteration) or the Hebrew letter ה:
// Replace final ה with ות (-ot)
// Feminine nouns ending in -et (transliteration) or ת-:
// Replace -et / ת with ות (-ot)
// Fallback:
// Add ות (-ot) covers other feminine patterns
//
// Notes:
// - Many Hebrew nouns have irregular plurals (e.g. ספר/ספרים, בית/בתים).
// The Engram vocabulary layer should supply these directly.
// - This function handles the productive, regular pattern.
fn he_pluralize(noun: String, gender: String) -> String {
if str_eq(gender, "m") {
// Masculine: add -im
return noun + "ים"
}
// Feminine noun ending in Hebrew ה (he) most common -a ending in script
if he_str_ends(noun, "ה") {
let stem: String = he_str_drop_last(noun, 1)
return stem + "ות"
}
// Feminine noun ending in ת (tav) covers -et, -at, -it endings
if he_str_ends(noun, "ת") {
let stem: String = he_str_drop_last(noun, 1)
return stem + "ות"
}
// Transliteration check: -a ending (e.g. "yalda" "yaldot")
if he_str_ends(noun, "a") {
let stem: String = he_str_drop_last(noun, 1)
return stem + "ot"
}
// Transliteration check: -et ending (e.g. "yaldet" "yaldot")
if he_str_ends(noun, "et") {
let stem: String = he_str_drop_last(noun, 2)
return stem + "ot"
}
// Fallback: add -ot
return noun + "ות"
}
// Definite noun phrases
//
// he_definite_prefix: attach the definite article ה (ha-) to a noun.
//
// The definite article in Hebrew is the prefix ה (ha-) attached directly
// to the noun without a space. In formal/Biblical Hebrew the following
// consonant receives a dagesh (doubling), but in Modern Hebrew pronunciation
// the doubling is generally not applied. We implement the simplified form:
// definite noun = "ה" + noun (script form)
// For transliterated nouns: "ha" + noun
//
// Callers pass Hebrew script nouns; transliterated nouns are handled by
// checking whether the noun starts with a Hebrew code-point range.
fn he_is_hebrew_script(noun: String) -> Bool {
// Hebrew Unicode block: U+05D0 (א) through U+05EA (ת).
// We check the first character: if str_len > 0 and it is a Hebrew letter,
// the noun is in Hebrew script. We use a set of common first letters as
// a heuristic; the alternative (numeric code-point comparison) is not
// available in El. This covers the vast majority of practical cases.
let n: Int = str_len(noun)
if n == 0 { return false }
let first: String = str_slice(noun, 0, 1)
// Common Hebrew first letters in the Unicode block
if str_eq(first, "א") { return true }
if str_eq(first, "ב") { return true }
if str_eq(first, "ג") { return true }
if str_eq(first, "ד") { return true }
if str_eq(first, "ה") { return true }
if str_eq(first, "ו") { return true }
if str_eq(first, "ז") { return true }
if str_eq(first, "ח") { return true }
if str_eq(first, "ט") { return true }
if str_eq(first, "י") { return true }
if str_eq(first, "כ") { return true }
if str_eq(first, "ל") { return true }
if str_eq(first, "מ") { return true }
if str_eq(first, "נ") { return true }
if str_eq(first, "ס") { return true }
if str_eq(first, "ע") { return true }
if str_eq(first, "פ") { return true }
if str_eq(first, "צ") { return true }
if str_eq(first, "ק") { return true }
if str_eq(first, "ר") { return true }
if str_eq(first, "ש") { return true }
if str_eq(first, "ת") { return true }
return false
}
fn he_definite_prefix(noun: String) -> String {
if he_is_hebrew_script(noun) {
return "ה" + noun
}
// Transliterated noun: prepend "ha"
return "ha" + noun
}
// he_noun_phrase: build a full noun phrase with definiteness and number.
//
// noun: base (singular) noun string (Hebrew script or transliteration)
// number: "singular" | "plural"
// gender: "m" | "f"
// definite: "true" | "false"
//
// Returns the surface noun phrase string.
fn he_noun_phrase(noun: String, number: String, gender: String, definite: String) -> String {
// Step 1: apply number (pluralize if needed)
let stem: String = noun
if str_eq(number, "plural") {
let stem = he_pluralize(noun, gender)
}
// Step 2: apply definiteness
if str_eq(definite, "true") {
return he_definite_prefix(stem)
}
return stem
}
// Canonical verb mapping
//
// he_map_canonical: map cross-lingual canonical English verb labels to
// their Hebrew equivalents before dispatching to he_conjugate.
//
// This mirrors morph_map_canonical in morphology.el but for Hebrew.
// Called by the morphology dispatcher before he_conjugate.
//
// Canonical labels: "be" | "have" | "do" | "go" | "see" | "eat" | "speak"
fn he_map_canonical(verb: String) -> String {
if str_eq(verb, "be") { return "lihyot" }
if str_eq(verb, "see") { return "lir'ot" }
if str_eq(verb, "eat") { return "le'exol" }
if str_eq(verb, "speak") { return "ledaber" }
if str_eq(verb, "say") { return "ledaber" }
if str_eq(verb, "go") { return "lalechet" }
// Unknown canonical: return as-is; he_conjugate will fall back to infinitive
return verb
}