Skip to main content

phonological_rules/
phonological_rules.rs

1//! Phonological Rules with FSTs
2//!
3//! This example demonstrates how to model phonological rule systems using Finite State
4//! Transducers (FSTs), following the foundational work of Kaplan and Kay (1994) and
5//! subsequent developments in computational phonology. It shows:
6//!
7//! 1. Building FSTs for individual phonological processes
8//! 2. Composing multiple phonological rules to model rule interaction
9//! 3. Classic phonological phenomena: vowel harmony, consonant cluster simplification,
10//!    vowel epenthesis, and final devoicing
11//! 4. Rule ordering effects and how composition order affects output
12//! 5. Complex multi-rule phonological systems
13//!
14//! This demonstrates the theoretical elegance and practical power of using FST
15//! composition to model how phonological rules interact in natural language systems.
16//!
17//! Based on the seminal work:
18//! - Kaplan, R. M., & Kay, M. (1994). Regular models of phonological rule systems.
19//! - Johnson, C. D. (1972). Formal aspects of phonological description.
20//! - Koskenniemi, K. (1983). Two-level morphology.
21//!
22//! Related examples:
23//! - morphological_analyzer.rs: Shows how phonological rules integrate with morphology
24//! - transliteration.rs: Demonstrates related phonetic transformation techniques
25//!
26//! Usage:
27//! ```bash
28//! cargo run --example phonological_rules
29//! ```
30
31use arcweight::prelude::*;
32
33/// Phonological feature representation
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[allow(dead_code)]
36enum PhonologicalFeature {
37    // Vowel features
38    High,
39    Mid,
40    Low,
41    Front,
42    Central,
43    Back,
44    Rounded,
45    Unrounded,
46
47    // Consonant features
48    Voiced,
49    Voiceless,
50    Stop,
51    Fricative,
52    Nasal,
53    Liquid,
54    Labial,
55    Coronal,
56    Dorsal,
57
58    // Prosodic features
59    Stressed,
60    Unstressed,
61
62    // Boundary markers
63    WordBoundary,
64    SyllableBoundary,
65}
66
67/// Phonological segment with features (for future extensions)
68#[derive(Debug, Clone)]
69#[allow(dead_code)] // Included for API completeness but not used in this example
70struct PhonologicalSegment {
71    symbol: char,
72    features: Vec<PhonologicalFeature>,
73}
74
75/// Phonological rule representation (for future extensions)
76#[derive(Debug, Clone)]
77#[allow(dead_code)] // Included for API completeness but not used in this example
78struct PhonologicalRule {
79    name: String,
80    description: String,
81    input_context: Vec<String>,
82    output_context: Vec<String>,
83    environment: Option<String>,
84}
85
86/// Build FST for vowel harmony (front/back spreading)
87/// Example: Turkish-style back vowel harmony
88fn build_vowel_harmony_fst() -> VectorFst<TropicalWeight> {
89    let mut fst = VectorFst::new();
90    let start = fst.add_state();
91    let back_state = fst.add_state();
92    let front_state = fst.add_state();
93
94    fst.set_start(start);
95    fst.set_final(start, TropicalWeight::one());
96    fst.set_final(back_state, TropicalWeight::one());
97    fst.set_final(front_state, TropicalWeight::one());
98
99    // Back vowels trigger back harmony
100    let back_vowels = ['a', 'o', 'u'];
101    let front_vowels = ['e', 'i'];
102
103    for &vowel in &back_vowels {
104        fst.add_arc(
105            start,
106            Arc::new(
107                vowel as u32,
108                vowel as u32,
109                TropicalWeight::one(),
110                back_state,
111            ),
112        );
113        fst.add_arc(
114            back_state,
115            Arc::new(
116                vowel as u32,
117                vowel as u32,
118                TropicalWeight::one(),
119                back_state,
120            ),
121        );
122    }
123
124    for &vowel in &front_vowels {
125        fst.add_arc(
126            start,
127            Arc::new(
128                vowel as u32,
129                vowel as u32,
130                TropicalWeight::one(),
131                front_state,
132            ),
133        );
134        fst.add_arc(
135            front_state,
136            Arc::new(
137                vowel as u32,
138                vowel as u32,
139                TropicalWeight::one(),
140                front_state,
141            ),
142        );
143    }
144
145    // Harmonizing vowel: 'E' becomes 'e' in front context, 'a' in back context
146    fst.add_arc(
147        back_state,
148        Arc::new('E' as u32, 'a' as u32, TropicalWeight::one(), back_state),
149    );
150
151    fst.add_arc(
152        front_state,
153        Arc::new('E' as u32, 'e' as u32, TropicalWeight::one(), front_state),
154    );
155
156    // Consonants are transparent
157    let consonants = "bcdfghjklmnpqrstvwxyz";
158    for ch in consonants.chars() {
159        fst.add_arc(
160            start,
161            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
162        );
163        fst.add_arc(
164            back_state,
165            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), back_state),
166        );
167        fst.add_arc(
168            front_state,
169            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), front_state),
170        );
171    }
172
173    fst
174}
175
176/// Build FST for consonant cluster simplification
177/// Example: /kt/ -> /t/ (cluster reduction)
178fn build_cluster_simplification_fst() -> VectorFst<TropicalWeight> {
179    let mut fst = VectorFst::new();
180    let start = fst.add_state();
181    let k_state = fst.add_state();
182
183    fst.set_start(start);
184    fst.set_final(start, TropicalWeight::one());
185
186    // /k/ followed by /t/ becomes just /t/
187    fst.add_arc(
188        start,
189        Arc::new(
190            'k' as u32,
191            0, // epsilon output (delete k)
192            TropicalWeight::one(),
193            k_state,
194        ),
195    );
196
197    fst.add_arc(
198        k_state,
199        Arc::new('t' as u32, 't' as u32, TropicalWeight::one(), start),
200    );
201
202    // All other characters pass through unchanged
203    for ch in b'a'..=b'z' {
204        let ch = ch as char;
205        if ch != 'k' {
206            fst.add_arc(
207                start,
208                Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
209            );
210        }
211    }
212
213    // k in other contexts passes through
214    for ch in b'a'..=b'z' {
215        let ch = ch as char;
216        if ch != 't' {
217            fst.add_arc(
218                k_state,
219                Arc::new(
220                    ch as u32,
221                    'k' as u32, // output the k we held
222                    TropicalWeight::one(),
223                    start,
224                ),
225            );
226            // Then process the current character
227            fst.add_arc(
228                start,
229                Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
230            );
231        }
232    }
233
234    fst
235}
236
237/// Build FST for vowel epenthesis (insertion)
238/// Example: Insert 'i' to break consonant clusters
239fn build_epenthesis_fst() -> VectorFst<TropicalWeight> {
240    let mut fst = VectorFst::new();
241    let start = fst.add_state();
242    let consonant_state = fst.add_state();
243
244    fst.set_start(start);
245    fst.set_final(start, TropicalWeight::one());
246    fst.set_final(consonant_state, TropicalWeight::one());
247
248    let vowels = "aeiou";
249    let consonants = "bcdfghjklmnpqrstvwxyz";
250
251    // Vowels pass through and reset to start
252    for ch in vowels.chars() {
253        fst.add_arc(
254            start,
255            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
256        );
257        fst.add_arc(
258            consonant_state,
259            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
260        );
261    }
262
263    // First consonant goes to consonant state
264    for ch in consonants.chars() {
265        fst.add_arc(
266            start,
267            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), consonant_state),
268        );
269    }
270
271    // Second consonant triggers epenthesis
272    for ch in consonants.chars() {
273        fst.add_arc(
274            consonant_state,
275            Arc::new(
276                ch as u32,
277                'i' as u32, // insert epenthetic vowel
278                TropicalWeight::one(),
279                start,
280            ),
281        );
282        // Then output the consonant
283        fst.add_arc(
284            start,
285            Arc::new(
286                0, // epsilon input
287                ch as u32,
288                TropicalWeight::one(),
289                consonant_state,
290            ),
291        );
292    }
293
294    fst
295}
296
297/// Build FST for final devoicing (German-style)
298/// Example: /d/ -> /t/ / _#
299fn build_final_devoicing_fst() -> VectorFst<TropicalWeight> {
300    let mut fst = VectorFst::new();
301    let start = fst.add_state();
302    let voiced_state = fst.add_state();
303
304    fst.set_start(start);
305    fst.set_final(start, TropicalWeight::one());
306
307    // Final voiced consonants become voiceless
308    fst.set_final(voiced_state, TropicalWeight::new(0.0)); // Cost for devoicing
309
310    let voiced_obstruents = ['b', 'd', 'g', 'z', 'v'];
311    let voiceless_obstruents = ['p', 't', 'k', 's', 'f'];
312
313    // Map voiced to voiceless at word end
314    for (&voiced, &voiceless) in voiced_obstruents.iter().zip(voiceless_obstruents.iter()) {
315        fst.add_arc(
316            start,
317            Arc::new(
318                voiced as u32,
319                voiceless as u32,
320                TropicalWeight::one(),
321                voiced_state,
322            ),
323        );
324    }
325
326    // All other characters pass through
327    for ch in b'a'..=b'z' {
328        let ch = ch as char;
329        if !voiced_obstruents.contains(&ch) {
330            fst.add_arc(
331                start,
332                Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
333            );
334        }
335    }
336
337    // Non-final voiced consonants pass through unchanged
338    for ch in b'a'..=b'z' {
339        let ch = ch as char;
340        fst.add_arc(
341            voiced_state,
342            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), start),
343        );
344    }
345
346    fst
347}
348
349/// Build FST that accepts a word
350fn build_word_fst(word: &str) -> VectorFst<TropicalWeight> {
351    let mut fst = VectorFst::new();
352    let mut current = fst.add_state();
353    fst.set_start(current);
354
355    for ch in word.chars() {
356        let next = fst.add_state();
357        fst.add_arc(
358            current,
359            Arc::new(ch as u32, ch as u32, TropicalWeight::one(), next),
360        );
361        current = next;
362    }
363
364    fst.set_final(current, TropicalWeight::one());
365    fst
366}
367
368/// Extract output string from FST path
369fn extract_output_string(fst: &VectorFst<TropicalWeight>) -> Option<String> {
370    if let Some(start) = fst.start() {
371        let mut result = String::new();
372        let mut current = start;
373        let mut visited = std::collections::HashSet::new();
374
375        loop {
376            if visited.contains(&current) {
377                break;
378            }
379            visited.insert(current);
380
381            if fst.is_final(current) {
382                return Some(result);
383            }
384
385            let mut found = false;
386            if let Some(arc) = fst.arcs(current).next() {
387                if arc.olabel != 0 {
388                    result.push(arc.olabel as u8 as char);
389                }
390                current = arc.nextstate;
391                found = true;
392            }
393
394            if !found {
395                break;
396            }
397        }
398    }
399
400    None
401}
402
403/// Apply a sequence of phonological rules via FST composition
404fn apply_phonological_rules(input: &str, rules: Vec<VectorFst<TropicalWeight>>) -> Result<String> {
405    let mut current_fst = build_word_fst(input);
406
407    println!("Applying phonological rules in sequence:");
408    println!("Input: '{input}'");
409
410    for (i, rule) in rules.iter().enumerate() {
411        let step = i + 1;
412        println!("\nStep {step}: Applying rule {step}");
413
414        // Compose current result with next rule
415        current_fst = compose_default(&current_fst, rule)?;
416
417        // Extract intermediate result
418        if let Some(intermediate) = extract_output_string(&current_fst) {
419            println!("  Result: '{intermediate}'");
420        } else {
421            println!("  Result: (no output)");
422        }
423    }
424
425    // Extract final result
426    if let Some(output) = extract_output_string(&current_fst) {
427        Ok(output)
428    } else {
429        Ok("(no output)".to_string())
430    }
431}
432
433fn main() -> Result<()> {
434    println!("Phonological Rules with FSTs");
435    println!("============================");
436    println!("Modeling phonological processes using Finite State Transducers");
437    println!("Based on Kaplan & Kay (1994) and subsequent work\n");
438
439    // Example 1: Turkish vowel harmony
440    println!("1. Turkish-style Vowel Harmony");
441    println!("------------------------------");
442    println!("Rule: Suffix vowel 'E' harmonizes with stem vowels");
443    println!("  Front vowels (e, i) → E becomes 'e'");
444    println!("  Back vowels (a, o, u) → E becomes 'a'");
445
446    let harmony_fst = build_vowel_harmony_fst();
447    let harmony_tests = vec!["kitabE", "evE", "adamE", "gelE"];
448
449    for test in harmony_tests {
450        let input_fst = build_word_fst(test);
451        let composed: VectorFst<TropicalWeight> = compose_default(&input_fst, &harmony_fst)?;
452        if let Some(output) = extract_output_string(&composed) {
453            println!("  '{test}' → '{output}'");
454        }
455    }
456
457    // Example 2: Consonant cluster simplification
458    println!("\n2. Consonant Cluster Simplification");
459    println!("-----------------------------------");
460    println!("Rule: /kt/ → /t/ (cluster reduction)");
461
462    let cluster_fst = build_cluster_simplification_fst();
463    let cluster_tests = vec!["akt", "ekte", "doktor", "katok"];
464
465    for test in cluster_tests {
466        let input_fst = build_word_fst(test);
467        let composed: VectorFst<TropicalWeight> = compose_default(&input_fst, &cluster_fst)?;
468        if let Some(output) = extract_output_string(&composed) {
469            println!("  '{test}' → '{output}'");
470        }
471    }
472
473    // Example 3: Vowel epenthesis
474    println!("\n3. Vowel Epenthesis");
475    println!("------------------");
476    println!("Rule: Insert 'i' between consonant clusters");
477
478    let epenthesis_fst = build_epenthesis_fst();
479    let epenthesis_tests = vec!["sport", "program", "strong"];
480
481    for test in epenthesis_tests {
482        let input_fst = build_word_fst(test);
483        let composed: VectorFst<TropicalWeight> = compose_default(&input_fst, &epenthesis_fst)?;
484        if let Some(output) = extract_output_string(&composed) {
485            println!("  '{test}' → '{output}'");
486        }
487    }
488
489    // Example 4: Final devoicing
490    println!("\n4. Final Devoicing (German-style)");
491    println!("---------------------------------");
492    println!("Rule: Voiced obstruents become voiceless word-finally");
493
494    let devoicing_fst = build_final_devoicing_fst();
495    let devoicing_tests = vec!["hund", "tag", "lieb", "haus"];
496
497    for test in devoicing_tests {
498        let input_fst = build_word_fst(test);
499        let composed: VectorFst<TropicalWeight> = compose_default(&input_fst, &devoicing_fst)?;
500        if let Some(output) = extract_output_string(&composed) {
501            println!("  '{test}' → '{output}'");
502        }
503    }
504
505    // Example 5: Rule composition and ordering
506    println!("\n5. Rule Interaction and Ordering");
507    println!("--------------------------------");
508    println!("Demonstrating how rule order affects output");
509
510    let test_word = "aktE";
511    println!("Input: '{test_word}'");
512
513    // Order 1: Harmony before cluster simplification
514    println!("\nOrder 1: Vowel Harmony → Cluster Simplification");
515    let rules1 = vec![harmony_fst.clone(), cluster_fst.clone()];
516    let result1 = apply_phonological_rules(test_word, rules1)?;
517    println!("Final result: '{result1}'");
518
519    // Order 2: Cluster simplification before harmony
520    println!("\nOrder 2: Cluster Simplification → Vowel Harmony");
521    let rules2 = vec![cluster_fst.clone(), harmony_fst.clone()];
522    let result2 = apply_phonological_rules(test_word, rules2)?;
523    println!("Final result: '{result2}'");
524
525    // Example 6: Complex rule interaction
526    println!("\n6. Complex Multi-Rule System");
527    println!("----------------------------");
528    println!("Applying multiple rules in sequence");
529
530    let complex_word = "sportE";
531    println!("Input: '{complex_word}'");
532
533    let all_rules = vec![
534        epenthesis_fst, // Break consonant clusters first
535        harmony_fst,    // Then apply vowel harmony
536        devoicing_fst,  // Finally apply final devoicing
537    ];
538
539    let final_result = apply_phonological_rules(complex_word, all_rules)?;
540    println!("Final result: '{final_result}'");
541
542    // Theoretical discussion
543    println!("\n7. Theoretical Background and Implications");
544    println!("-----------------------------------------");
545    println!("This example demonstrates key insights from computational phonology:");
546    println!("  • Phonological rules as regular relations (Kaplan & Kay, 1994)");
547    println!("  • Rule application through FST composition");
548    println!("  • Natural emergence of rule ordering effects from composition order");
549    println!("  • Modeling of opacity, transparency, and bleeding/feeding interactions");
550    println!("  • Bidirectional processing: generation ↔ recognition");
551    println!("  • Connection to two-level morphology (Koskenniemi, 1983)");
552
553    println!("\nHistorical development:");
554    println!("  • Johnson (1972): Early formal approaches to phonological rules");
555    println!("  • Koskenniemi (1983): Two-level morphology with FSTs");
556    println!("  • Kaplan & Kay (1994): Regular models of phonological rule systems");
557    println!("  • Modern applications: Finite-state phonology in NLP systems");
558
559    println!("\nApplications in computational linguistics:");
560    println!("  • Morphophonological analysis and generation");
561    println!("  • Text-to-speech synthesis systems");
562    println!("  • Automatic speech recognition");
563    println!("  • Historical linguistics and sound change modeling");
564    println!("  • Language documentation and endangered language preservation");
565    println!("  • Cross-linguistic phonological typology studies");
566    println!("  • Psycholinguistic modeling of phonological processing");
567
568    Ok(())
569}