Skip to main content

lindera_dictionary/
viterbi.rs

1use std::io;
2
3use byteorder::{ByteOrder, LittleEndian, WriteBytesExt};
4use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
5use serde::{Deserialize, Serialize};
6
7use crate::dictionary::character_definition::{CategoryId, CharacterDefinition};
8use crate::dictionary::connection_cost_matrix::ConnectionCostMatrix;
9use crate::dictionary::prefix_dictionary::PrefixDictionary;
10use crate::dictionary::unknown_dictionary::UnknownDictionary;
11use crate::mode::Mode;
12
13/// Type of lexicon containing the word
14#[derive(
15    Clone,
16    Copy,
17    Debug,
18    Eq,
19    PartialEq,
20    Serialize,
21    Deserialize,
22    Default,
23    Archive,
24    RkyvSerialize,
25    RkyvDeserialize,
26)]
27
28pub enum LexType {
29    /// System dictionary (base dictionary)
30    #[default]
31    System,
32    /// User dictionary (additional vocabulary)
33    User,
34    /// Unknown words (OOV handling)
35    Unknown,
36}
37
38#[derive(
39    Clone,
40    Copy,
41    Debug,
42    Eq,
43    PartialEq,
44    Serialize,
45    Deserialize,
46    Archive,
47    RkyvDeserialize,
48    RkyvSerialize,
49)]
50
51pub struct WordId {
52    /// Numeric identifier of the word within its lexicon.
53    id: u32,
54    /// Whether the word originates from the system dictionary.
55    is_system: bool,
56    /// Lexicon type the word belongs to.
57    lex_type: LexType,
58}
59
60impl WordId {
61    /// Creates a new WordId with specified lexicon type
62    pub fn new(lex_type: LexType, id: u32) -> Self {
63        WordId {
64            id,
65            is_system: matches!(lex_type, LexType::System),
66            lex_type,
67        }
68    }
69
70    /// Returns the numeric identifier of the word within its lexicon.
71    ///
72    /// # 戻り値
73    ///
74    /// The lexicon-local word id.
75    #[inline]
76    pub fn id(&self) -> u32 {
77        self.id
78    }
79
80    /// Returns `true` when the word is an unknown-word entry.
81    #[inline]
82    pub fn is_unknown(&self) -> bool {
83        matches!(self.lex_type, LexType::Unknown)
84    }
85
86    /// Returns `true` when the word originates from the system dictionary.
87    #[inline]
88    pub fn is_system(&self) -> bool {
89        self.is_system
90    }
91
92    /// Returns the lexicon type of the word.
93    #[inline]
94    pub fn lex_type(&self) -> LexType {
95        self.lex_type
96    }
97}
98
99impl Default for WordId {
100    fn default() -> Self {
101        WordId {
102            id: u32::MAX,
103            is_system: true,
104            lex_type: LexType::System,
105        }
106    }
107}
108
109#[derive(
110    Default,
111    Clone,
112    Copy,
113    Debug,
114    Eq,
115    PartialEq,
116    Serialize,
117    Deserialize,
118    Archive,
119    RkyvSerialize,
120    RkyvDeserialize,
121)]
122
123pub struct WordEntry {
124    /// Word id identifying this entry in the dictionary.
125    word_id: WordId,
126    /// Emission (word) cost of the entry.
127    word_cost: i16,
128    /// Left context id used by the connection matrix.
129    left_id: u16,
130    /// Right context id used by the connection matrix.
131    right_id: u16,
132}
133
134impl WordEntry {
135    /// Length in bytes of the serialized representation.
136    pub(crate) const SERIALIZED_LEN: usize = 10;
137
138    /// Creates a new word entry from its raw components.
139    ///
140    /// # 引数
141    ///
142    /// * `word_id` - The word id identifying the entry.
143    /// * `word_cost` - The emission cost of the word.
144    /// * `left_id` - The left context id.
145    /// * `right_id` - The right context id.
146    #[inline]
147    pub fn new(word_id: WordId, word_cost: i16, left_id: u16, right_id: u16) -> Self {
148        WordEntry {
149            word_id,
150            word_cost,
151            left_id,
152            right_id,
153        }
154    }
155
156    /// Returns the word id of this entry.
157    #[inline]
158    pub fn word_id(&self) -> WordId {
159        self.word_id
160    }
161
162    /// Returns the emission (word) cost of this entry.
163    #[inline]
164    pub fn word_cost(&self) -> i16 {
165        self.word_cost
166    }
167
168    /// Returns the left context id, widened to `u32`.
169    #[inline]
170    pub fn left_id(&self) -> u32 {
171        self.left_id as u32
172    }
173
174    /// Returns the right context id, widened to `u32`.
175    #[inline]
176    pub fn right_id(&self) -> u32 {
177        self.right_id as u32
178    }
179
180    /// Serializes this entry into `wtr` in little-endian byte order.
181    pub(crate) fn serialize<W: io::Write>(&self, wtr: &mut W) -> io::Result<()> {
182        wtr.write_u32::<LittleEndian>(self.word_id.id)?;
183        wtr.write_i16::<LittleEndian>(self.word_cost)?;
184        wtr.write_u16::<LittleEndian>(self.left_id)?;
185        wtr.write_u16::<LittleEndian>(self.right_id)?;
186        Ok(())
187    }
188
189    /// Deserializes a word entry from `data`.
190    pub(crate) fn deserialize(data: &[u8], is_system_entry: bool) -> WordEntry {
191        let word_id = WordId::new(
192            if is_system_entry {
193                LexType::System
194            } else {
195                LexType::User
196            },
197            LittleEndian::read_u32(&data[0..4]),
198        );
199        let word_cost = LittleEndian::read_i16(&data[4..6]);
200        let left_id = LittleEndian::read_u16(&data[6..8]);
201        let right_id = LittleEndian::read_u16(&data[8..10]);
202        WordEntry {
203            word_id,
204            word_cost,
205            left_id,
206            right_id,
207        }
208    }
209}
210
211#[derive(Default, Clone, Debug)]
212pub struct Edge {
213    /// Word entry backing this edge.
214    word_entry: WordEntry,
215
216    /// Best forward path cost reaching this edge.
217    path_cost: i32,
218    /// Index of the chosen left edge in the previous position's vector.
219    left_index: u16,
220
221    /// Start byte position of the edge.
222    start_index: u32,
223    /// Stop byte position of the edge.
224    stop_index: u32,
225
226    /// Whether the edge surface consists solely of kanji.
227    kanji_only: bool,
228}
229
230impl Edge {
231    /// Returns the number of characters spanned by this edge.
232    pub fn num_chars(&self) -> usize {
233        (self.stop_index - self.start_index) as usize / 3
234    }
235
236    /// Returns the word entry backing this edge.
237    #[inline]
238    pub(crate) fn word_entry(&self) -> &WordEntry {
239        &self.word_entry
240    }
241
242    /// Returns the best forward path cost reaching this edge.
243    #[inline]
244    pub(crate) fn path_cost(&self) -> i32 {
245        self.path_cost
246    }
247
248    /// Returns the index of the chosen left edge in the previous position.
249    #[inline]
250    pub(crate) fn left_index(&self) -> u16 {
251        self.left_index
252    }
253
254    /// Returns the start byte position of this edge.
255    #[inline]
256    pub(crate) fn start_index(&self) -> u32 {
257        self.start_index
258    }
259
260    /// Returns the stop byte position of this edge.
261    #[inline]
262    pub(crate) fn stop_index(&self) -> u32 {
263        self.stop_index
264    }
265
266    /// Returns whether the edge surface consists solely of kanji.
267    #[inline]
268    pub(crate) fn kanji_only(&self) -> bool {
269        self.kanji_only
270    }
271}
272
273/// Records a transition from a left edge to the current edge.
274/// Used in N-Best mode to store all predecessor transitions
275/// (not just the best one as in 1-best).
276#[derive(Clone, Debug)]
277pub struct PathEntry {
278    /// Index of this edge in ends_at[stop_index]
279    edge_index: u16,
280    /// Byte position where the left edge ends (= this edge's start_index)
281    left_pos: u32,
282    /// Index of the left edge in ends_at[left_pos]
283    left_index: u16,
284    /// Total forward cost: left_edge.path_cost + conn_cost + penalty_cost
285    cost: i32,
286}
287
288impl PathEntry {
289    /// Returns the index of this edge in `ends_at[stop_index]`.
290    #[inline]
291    pub(crate) fn edge_index(&self) -> u16 {
292        self.edge_index
293    }
294
295    /// Returns the byte position where the left edge ends.
296    #[inline]
297    pub(crate) fn left_pos(&self) -> u32 {
298        self.left_pos
299    }
300
301    /// Returns the index of the left edge in `ends_at[left_pos]`.
302    #[inline]
303    pub(crate) fn left_index(&self) -> u16 {
304        self.left_index
305    }
306
307    /// Returns the total forward cost of this transition.
308    #[inline]
309    pub(crate) fn cost(&self) -> i32 {
310        self.cost
311    }
312}
313
314#[derive(Clone, Default)]
315pub struct Lattice {
316    capacity: usize,
317    ends_at: Vec<Vec<Edge>>, // Now stores edges directly
318    char_info_buffer: Vec<CharData>,
319    categories_buffer: Vec<CategoryId>,
320    char_category_cache: Vec<Vec<CategoryId>>,
321
322    // N-Best fields (only populated when set_text_nbest is called)
323    all_paths: Vec<Vec<PathEntry>>,
324    nbest_capacity: usize,
325    /// The text length (in bytes) of the last set_text/set_text_nbest call
326    last_text_len: usize,
327
328    // Scratch buffers for the Aho-Corasick match pre-scan in set_text/set_text_nbest.
329    // Reused across calls (like the fields above) instead of being reallocated per
330    // call, since set_text runs once per sentence rather than once per document.
331    /// Linked-list head table: matches_head[start_idx] -> index into matches_store.
332    matches_head: Vec<usize>,
333    /// Linked-list node pool: (match end offset, word entry, next node index).
334    matches_store: Vec<(usize, WordEntry, usize)>,
335}
336
337#[derive(Clone, Copy, Debug, Default)]
338struct CharData {
339    byte_offset: u32,
340    is_kanji: bool,
341    categories_start: u32,
342    categories_len: u16,
343    kanji_run_byte_len: u32,
344}
345
346#[inline]
347pub fn is_kanji(c: char) -> bool {
348    let c = c as u32;
349    // CJK Unified Ideographs (4E00-9FAF) and Extension A (3400-4DBF)
350    (0x4E00..=0x9FAF).contains(&c) || (0x3400..=0x4DBF).contains(&c)
351}
352
353impl Lattice {
354    /// Helper method to create an edge efficiently
355    #[inline]
356    fn create_edge(word_entry: WordEntry, start: usize, stop: usize, kanji_only: bool) -> Edge {
357        Edge {
358            word_entry,
359            left_index: u16::MAX,
360            start_index: start as u32,
361            stop_index: stop as u32,
362            path_cost: i32::MAX,
363            kanji_only,
364        }
365    }
366
367    pub fn clear(&mut self) {
368        for edge_vec in &mut self.ends_at {
369            edge_vec.clear();
370        }
371        for path_vec in &mut self.all_paths {
372            path_vec.clear();
373        }
374        self.char_info_buffer.clear();
375        self.categories_buffer.clear();
376        // `char_category_cache` is keyed only by codepoint, with no identity
377        // of the `CharacterDefinition` it was filled from. Since a `Lattice`
378        // can be reused across `Segmenter`s built from different
379        // dictionaries, the cache must be invalidated every call rather than
380        // persisting across `set_text`/`set_text_nbest` invocations -- an
381        // ASCII codepoint's `CategoryId` is not portable between
382        // dictionaries with different char.def category orderings.
383        self.char_category_cache.clear();
384    }
385
386    #[inline]
387    fn is_kanji_all(&self, char_idx: usize, byte_len: usize) -> bool {
388        self.char_info_buffer[char_idx].kanji_run_byte_len >= byte_len as u32
389    }
390
391    #[inline]
392    fn get_cached_category(&self, char_idx: usize, category_ord: usize) -> CategoryId {
393        let char_data = &self.char_info_buffer[char_idx];
394        self.categories_buffer[char_data.categories_start as usize + category_ord]
395    }
396
397    fn set_capacity(&mut self, text_len: usize) {
398        self.clear();
399        self.last_text_len = text_len;
400        if self.capacity <= text_len {
401            self.capacity = text_len;
402            // Pre-size newly-grown slots (like Vibrato's reset_vec) to
403            // avoid a couple of small reallocations the first time a busy
404            // position accumulates several edges. `resize_with` is required
405            // here: `resize` fills new slots with clones of its template
406            // value, and cloning an empty Vec allocates capacity 0, so only
407            // the moved-in last slot would actually be pre-sized (#827).
408            self.ends_at
409                .resize_with(text_len + 1, || Vec::with_capacity(16));
410        }
411    }
412
413    fn set_capacity_nbest(&mut self, text_len: usize) {
414        self.set_capacity(text_len);
415        if self.nbest_capacity <= text_len {
416            self.nbest_capacity = text_len;
417            self.all_paths.resize(text_len + 1, Vec::new());
418        }
419    }
420
421    #[inline(never)]
422    // Forward Viterbi implementation:
423    // Constructs the lattice and calculates the path costs simultaneously.
424    // This improves performance by avoiding a separate lattice traversal pass.
425    #[allow(clippy::too_many_arguments)]
426    pub fn set_text(
427        &mut self,
428        dict: &PrefixDictionary,
429        user_dict: &Option<&PrefixDictionary>,
430        char_definitions: &CharacterDefinition,
431        unknown_dictionary: &UnknownDictionary,
432        cost_matrix: &ConnectionCostMatrix,
433        text: &str,
434        search_mode: &Mode,
435    ) {
436        let len = text.len();
437        self.set_capacity(len);
438
439        // Pre-calculate character information for the text
440        self.char_info_buffer.clear();
441        self.categories_buffer.clear();
442
443        if self.char_category_cache.is_empty() {
444            self.char_category_cache.resize(256, Vec::new());
445        }
446
447        for (byte_offset, c) in text.char_indices() {
448            let categories_start = self.categories_buffer.len() as u32;
449
450            if (c as u32) < 256 {
451                let cached = &mut self.char_category_cache[c as usize];
452                if cached.is_empty() {
453                    let cats = char_definitions.lookup_categories(c);
454                    for &category in cats {
455                        cached.push(category);
456                    }
457                }
458                for &category in cached.iter() {
459                    self.categories_buffer.push(category);
460                }
461            } else {
462                let categories = char_definitions.lookup_categories(c);
463                for &category in categories {
464                    self.categories_buffer.push(category);
465                }
466            }
467
468            let categories_len = (self.categories_buffer.len() as u32 - categories_start) as u16;
469
470            self.char_info_buffer.push(CharData {
471                byte_offset: byte_offset as u32,
472                is_kanji: is_kanji(c),
473                categories_start,
474                categories_len,
475                kanji_run_byte_len: 0,
476            });
477        }
478        // Sentinel for end of text
479        self.char_info_buffer.push(CharData {
480            byte_offset: len as u32,
481            is_kanji: false,
482            categories_start: 0,
483            categories_len: 0,
484            kanji_run_byte_len: 0,
485        });
486
487        // Pre-calculate Kanji run lengths (backwards)
488        for i in (0..self.char_info_buffer.len() - 1).rev() {
489            if self.char_info_buffer[i].is_kanji {
490                let next_byte_offset = self.char_info_buffer[i + 1].byte_offset;
491                let char_byte_len = next_byte_offset - self.char_info_buffer[i].byte_offset;
492                self.char_info_buffer[i].kanji_run_byte_len =
493                    char_byte_len + self.char_info_buffer[i + 1].kanji_run_byte_len;
494            } else {
495                self.char_info_buffer[i].kanji_run_byte_len = 0;
496            }
497        }
498
499        let start_edge = Edge {
500            path_cost: 0,
501            left_index: u16::MAX,
502            ..Default::default()
503        };
504        self.ends_at[0].push(start_edge);
505
506        // Index of the last character of unknown word
507        let mut unknown_word_end: Option<usize> = None;
508
509        // Pre-scan text with Aho-Corasick to report all matches
510        // Optimization: Use flat vectors instead of Vec<Vec<_>> to avoid many small allocations.
511        // Linked list structure: matches_head[start_idx] -> index in matches_store
512        // Buffers are Lattice fields reused across calls; refill matches_head (its
513        // contents are meaningful, unlike ends_at's empty-Vec slots) and clear
514        // matches_store (a plain append-only pool).
515        self.matches_head.clear();
516        self.matches_head.resize(len + 1, usize::MAX);
517        self.matches_store.clear();
518
519        // System dictionary scan
520        for m in dict.da.find_overlapping_iter(text) {
521            let start = m.start();
522            let (offset, count) = dict.decode_val(m.value());
523            let offset_bytes = (offset as usize) * WordEntry::SERIALIZED_LEN;
524
525            // Bounds check for safety, though daachorse should guarantee valid ids if built correctly
526            if offset_bytes < dict.vals_data.len() {
527                let data_slice = &dict.vals_data[offset_bytes..];
528                for i in 0..count {
529                    let entry_offset = WordEntry::SERIALIZED_LEN * (i as usize);
530                    if entry_offset + WordEntry::SERIALIZED_LEN <= data_slice.len() {
531                        let entry = WordEntry::deserialize(&data_slice[entry_offset..], true);
532                        if start < self.matches_head.len() {
533                            let next = self.matches_head[start];
534                            self.matches_head[start] = self.matches_store.len();
535                            self.matches_store.push((m.end(), entry, next));
536                        }
537                    }
538                }
539            }
540        }
541
542        // User dictionary scan
543        if let Some(ud) = user_dict {
544            for m in ud.da.find_overlapping_iter(text) {
545                let start = m.start();
546                let (offset, count) = ud.decode_val(m.value());
547                let offset_bytes = (offset as usize) * WordEntry::SERIALIZED_LEN;
548
549                if offset_bytes < ud.vals_data.len() {
550                    let data_slice = &ud.vals_data[offset_bytes..];
551                    for i in 0..count {
552                        let entry_offset = WordEntry::SERIALIZED_LEN * (i as usize);
553                        if entry_offset + WordEntry::SERIALIZED_LEN <= data_slice.len() {
554                            let entry = WordEntry::deserialize(&data_slice[entry_offset..], false);
555                            if start < self.matches_head.len() {
556                                let next = self.matches_head[start];
557                                self.matches_head[start] = self.matches_store.len();
558                                self.matches_store.push((m.end(), entry, next));
559                            }
560                        }
561                    }
562                }
563            }
564        }
565
566        for char_idx in 0..self.char_info_buffer.len() - 1 {
567            let start = self.char_info_buffer[char_idx].byte_offset as usize;
568
569            // No arc is ending here.
570            // No need to check if a valid word starts here.
571            if self.ends_at[start].is_empty() {
572                continue;
573            }
574
575            let mut found: bool = false;
576
577            // Use cached matches
578            if start < self.matches_head.len() {
579                let mut match_idx = self.matches_head[start];
580                while match_idx != usize::MAX {
581                    let (end, word_entry, next) = self.matches_store[match_idx];
582
583                    let prefix_len = end - start;
584                    let kanji_only = self.is_kanji_all(char_idx, prefix_len);
585                    let edge = Self::create_edge(
586                        word_entry, // WordEntry is Copy
587                        start, end, kanji_only,
588                    );
589                    self.add_edge_in_lattice(edge, cost_matrix, search_mode);
590                    found = true;
591
592                    match_idx = next;
593                }
594            }
595
596            // In the case of normal mode, it doesn't process unknown word greedily.
597            if (search_mode.is_search()
598                || unknown_word_end.map(|index| index <= start).unwrap_or(true))
599                && char_idx < self.char_info_buffer.len() - 1
600            {
601                let num_categories = self.char_info_buffer[char_idx].categories_len as usize;
602                for category_ord in 0..num_categories {
603                    let category = self.get_cached_category(char_idx, category_ord);
604                    unknown_word_end = self.process_unknown_word(
605                        char_definitions,
606                        unknown_dictionary,
607                        cost_matrix,
608                        search_mode,
609                        category,
610                        category_ord,
611                        unknown_word_end,
612                        start,
613                        char_idx,
614                        found,
615                    );
616                }
617            }
618        }
619
620        // Connect EOS
621        if !self.ends_at[len].is_empty() {
622            let mut eos_edge = Edge {
623                start_index: len as u32,
624                stop_index: len as u32,
625                ..Default::default()
626            };
627            // Calculate cost for EOS
628            let left_edges = &self.ends_at[len];
629            let mut best_cost = i32::MAX;
630            let mut best_left = None;
631            let right_left_id = 0; // EOS default left_id
632
633            for (i, left_edge) in left_edges.iter().enumerate() {
634                let left_right_id = left_edge.word_entry.right_id();
635                let conn_cost = cost_matrix.cost(left_right_id, right_left_id);
636                let path_cost = left_edge.path_cost.saturating_add(conn_cost);
637                if path_cost < best_cost {
638                    best_cost = path_cost;
639                    best_left = Some(i as u16);
640                }
641            }
642            if let Some(left_idx) = best_left {
643                eos_edge.left_index = left_idx;
644                eos_edge.path_cost = best_cost;
645                self.ends_at[len].push(eos_edge);
646            }
647        }
648    }
649
650    #[allow(clippy::too_many_arguments)]
651    fn process_unknown_word(
652        &mut self,
653        char_definitions: &CharacterDefinition,
654        unknown_dictionary: &UnknownDictionary,
655        cost_matrix: &ConnectionCostMatrix,
656        search_mode: &Mode,
657        category: CategoryId,
658        category_ord: usize,
659        unknown_word_index: Option<usize>,
660        start: usize,
661        char_idx: usize,
662        found: bool,
663    ) -> Option<usize> {
664        let mut unknown_word_num_chars: usize = 0;
665        let category_data = char_definitions.lookup_definition(category);
666        if category_data.invoke || !found {
667            unknown_word_num_chars = 1;
668            if category_data.group {
669                for i in 1.. {
670                    let next_idx = char_idx + i;
671                    if next_idx >= self.char_info_buffer.len() - 1 {
672                        break;
673                    }
674                    let num_categories = self.char_info_buffer[next_idx].categories_len as usize;
675                    let mut found_cat = false;
676                    if category_ord < num_categories {
677                        let cat = self.get_cached_category(next_idx, category_ord);
678                        if cat == category {
679                            unknown_word_num_chars += 1;
680                            found_cat = true;
681                        }
682                    }
683                    if !found_cat {
684                        break;
685                    }
686                }
687            }
688        }
689        if unknown_word_num_chars > 0 {
690            let byte_end_offset =
691                self.char_info_buffer[char_idx + unknown_word_num_chars].byte_offset;
692            let byte_len = byte_end_offset as usize - start;
693
694            // Check Kanji status using pre-calculated buffer
695            let kanji_only = self.is_kanji_all(char_idx, byte_len);
696
697            for &word_id in unknown_dictionary.lookup_word_ids(category) {
698                let word_entry = unknown_dictionary.word_entry(word_id);
699                let edge = Self::create_edge(word_entry, start, start + byte_len, kanji_only);
700                self.add_edge_in_lattice(edge, cost_matrix, search_mode);
701            }
702            return Some(start + byte_len);
703        }
704        unknown_word_index
705    }
706
707    // Adds an edge to the lattice and calculates the minimum cost to reach it.
708    fn add_edge_in_lattice(
709        &mut self,
710        mut edge: Edge,
711        cost_matrix: &ConnectionCostMatrix,
712        mode: &Mode,
713    ) {
714        let start_index = edge.start_index as usize;
715        let stop_index = edge.stop_index as usize;
716        let right_left_id = edge.word_entry.left_id();
717
718        let left_edges = &self.ends_at[start_index];
719        if left_edges.is_empty() {
720            return;
721        }
722
723        let mut best_cost = i32::MAX;
724        let mut best_left = None;
725
726        match mode {
727            Mode::Normal => {
728                for (i, left_edge) in left_edges.iter().enumerate() {
729                    let left_right_id = left_edge.word_entry.right_id();
730                    let conn_cost = cost_matrix.cost(left_right_id, right_left_id);
731                    let total_cost = left_edge.path_cost.saturating_add(conn_cost);
732
733                    if total_cost < best_cost {
734                        best_cost = total_cost;
735                        best_left = Some(i as u16);
736                    }
737                }
738            }
739            Mode::Decompose(penalty) => {
740                for (i, left_edge) in left_edges.iter().enumerate() {
741                    let left_right_id = left_edge.word_entry.right_id();
742                    let conn_cost = cost_matrix.cost(left_right_id, right_left_id);
743                    let penalty_cost = penalty.penalty(left_edge);
744                    let total_cost = left_edge
745                        .path_cost
746                        .saturating_add(conn_cost)
747                        .saturating_add(penalty_cost);
748
749                    if total_cost < best_cost {
750                        best_cost = total_cost;
751                        best_left = Some(i as u16);
752                    }
753                }
754            }
755        }
756
757        if let Some(best_left_idx) = best_left {
758            edge.path_cost = best_cost.saturating_add(edge.word_entry.word_cost as i32);
759            edge.left_index = best_left_idx;
760            self.ends_at[stop_index].push(edge);
761        }
762    }
763
764    pub fn tokens_offset(&self) -> Vec<(usize, WordId)> {
765        let mut offsets = Vec::new();
766
767        if self.ends_at.is_empty() {
768            return offsets;
769        }
770
771        let mut last_idx = self.ends_at.len() - 1;
772        while last_idx > 0 && self.ends_at[last_idx].is_empty() {
773            last_idx -= 1;
774        }
775
776        if self.ends_at[last_idx].is_empty() {
777            return offsets;
778        }
779
780        let idx = self.ends_at[last_idx].len() - 1;
781        let mut edge = &self.ends_at[last_idx][idx];
782
783        if edge.left_index == u16::MAX {
784            return offsets;
785        }
786
787        loop {
788            if edge.left_index == u16::MAX {
789                break;
790            }
791
792            offsets.push((edge.start_index as usize, edge.word_entry.word_id));
793
794            let left_idx = edge.left_index as usize;
795            let start_idx = edge.start_index as usize;
796
797            edge = &self.ends_at[start_idx][left_idx];
798        }
799
800        offsets.reverse();
801        offsets.pop(); // Remove EOS
802
803        offsets
804    }
805
806    // --- N-Best support ---
807
808    /// Returns the text length (in bytes) from the last set_text/set_text_nbest call.
809    pub fn text_len(&self) -> usize {
810        self.last_text_len
811    }
812
813    /// Returns the edges at a given byte position.
814    pub fn edges_at(&self, byte_pos: usize) -> &[Edge] {
815        &self.ends_at[byte_pos]
816    }
817
818    /// Returns the N-Best path entries at a given byte position.
819    pub fn paths_at(&self, byte_pos: usize) -> &[PathEntry] {
820        if byte_pos < self.all_paths.len() {
821            &self.all_paths[byte_pos]
822        } else {
823            &[]
824        }
825    }
826
827    /// Adds an edge to the lattice, recording ALL predecessor transitions for N-Best.
828    fn add_edge_in_lattice_nbest(
829        &mut self,
830        mut edge: Edge,
831        cost_matrix: &ConnectionCostMatrix,
832        mode: &Mode,
833    ) {
834        let start_index = edge.start_index as usize;
835        let stop_index = edge.stop_index as usize;
836        let right_left_id = edge.word_entry.left_id();
837
838        let left_edges = &self.ends_at[start_index];
839        if left_edges.is_empty() {
840            return;
841        }
842
843        let mut best_cost = i32::MAX;
844        let mut best_left = None;
845
846        // The edge_index of the new edge being added
847        let new_edge_index = self.ends_at[stop_index].len() as u16;
848
849        match mode {
850            Mode::Normal => {
851                for (i, left_edge) in left_edges.iter().enumerate() {
852                    let left_right_id = left_edge.word_entry.right_id();
853                    let conn_cost = cost_matrix.cost(left_right_id, right_left_id);
854                    let total_cost = left_edge.path_cost.saturating_add(conn_cost);
855
856                    // Record ALL transitions for N-Best
857                    self.all_paths[stop_index].push(PathEntry {
858                        edge_index: new_edge_index,
859                        left_pos: start_index as u32,
860                        left_index: i as u16,
861                        cost: total_cost,
862                    });
863
864                    if total_cost < best_cost {
865                        best_cost = total_cost;
866                        best_left = Some(i as u16);
867                    }
868                }
869            }
870            Mode::Decompose(penalty) => {
871                for (i, left_edge) in left_edges.iter().enumerate() {
872                    let left_right_id = left_edge.word_entry.right_id();
873                    let conn_cost = cost_matrix.cost(left_right_id, right_left_id);
874                    let penalty_cost = penalty.penalty(left_edge);
875                    let total_cost = left_edge
876                        .path_cost
877                        .saturating_add(conn_cost)
878                        .saturating_add(penalty_cost);
879
880                    // Record ALL transitions for N-Best
881                    self.all_paths[stop_index].push(PathEntry {
882                        edge_index: new_edge_index,
883                        left_pos: start_index as u32,
884                        left_index: i as u16,
885                        cost: total_cost,
886                    });
887
888                    if total_cost < best_cost {
889                        best_cost = total_cost;
890                        best_left = Some(i as u16);
891                    }
892                }
893            }
894        }
895
896        if let Some(best_left_idx) = best_left {
897            edge.path_cost = best_cost.saturating_add(edge.word_entry.word_cost as i32);
898            edge.left_index = best_left_idx;
899            self.ends_at[stop_index].push(edge);
900        }
901    }
902
903    #[allow(clippy::too_many_arguments)]
904    fn process_unknown_word_nbest(
905        &mut self,
906        char_definitions: &CharacterDefinition,
907        unknown_dictionary: &UnknownDictionary,
908        cost_matrix: &ConnectionCostMatrix,
909        search_mode: &Mode,
910        category: CategoryId,
911        category_ord: usize,
912        unknown_word_index: Option<usize>,
913        start: usize,
914        char_idx: usize,
915        found: bool,
916    ) -> Option<usize> {
917        let mut unknown_word_num_chars: usize = 0;
918        let category_data = char_definitions.lookup_definition(category);
919        if category_data.invoke || !found {
920            unknown_word_num_chars = 1;
921            if category_data.group {
922                for i in 1.. {
923                    let next_idx = char_idx + i;
924                    if next_idx >= self.char_info_buffer.len() - 1 {
925                        break;
926                    }
927                    let num_categories = self.char_info_buffer[next_idx].categories_len as usize;
928                    let mut found_cat = false;
929                    if category_ord < num_categories {
930                        let cat = self.get_cached_category(next_idx, category_ord);
931                        if cat == category {
932                            unknown_word_num_chars += 1;
933                            found_cat = true;
934                        }
935                    }
936                    if !found_cat {
937                        break;
938                    }
939                }
940            }
941        }
942        if unknown_word_num_chars > 0 {
943            let byte_end_offset =
944                self.char_info_buffer[char_idx + unknown_word_num_chars].byte_offset;
945            let byte_len = byte_end_offset as usize - start;
946
947            let kanji_only = self.is_kanji_all(char_idx, byte_len);
948
949            for &word_id in unknown_dictionary.lookup_word_ids(category) {
950                let word_entry = unknown_dictionary.word_entry(word_id);
951                let edge = Self::create_edge(word_entry, start, start + byte_len, kanji_only);
952                self.add_edge_in_lattice_nbest(edge, cost_matrix, search_mode);
953            }
954            return Some(start + byte_len);
955        }
956        unknown_word_index
957    }
958
959    /// Forward Viterbi implementation for N-Best mode.
960    /// Same as set_text() but records ALL predecessor transitions in all_paths.
961    #[inline(never)]
962    #[allow(clippy::too_many_arguments)]
963    pub fn set_text_nbest(
964        &mut self,
965        dict: &PrefixDictionary,
966        user_dict: &Option<&PrefixDictionary>,
967        char_definitions: &CharacterDefinition,
968        unknown_dictionary: &UnknownDictionary,
969        cost_matrix: &ConnectionCostMatrix,
970        text: &str,
971        search_mode: &Mode,
972    ) {
973        let len = text.len();
974        self.set_capacity_nbest(len);
975
976        // Pre-calculate character information for the text
977        self.char_info_buffer.clear();
978        self.categories_buffer.clear();
979
980        if self.char_category_cache.is_empty() {
981            self.char_category_cache.resize(256, Vec::new());
982        }
983
984        for (byte_offset, c) in text.char_indices() {
985            let categories_start = self.categories_buffer.len() as u32;
986
987            if (c as u32) < 256 {
988                let cached = &mut self.char_category_cache[c as usize];
989                if cached.is_empty() {
990                    let cats = char_definitions.lookup_categories(c);
991                    for &category in cats {
992                        cached.push(category);
993                    }
994                }
995                for &category in cached.iter() {
996                    self.categories_buffer.push(category);
997                }
998            } else {
999                let categories = char_definitions.lookup_categories(c);
1000                for &category in categories {
1001                    self.categories_buffer.push(category);
1002                }
1003            }
1004
1005            let categories_len = (self.categories_buffer.len() as u32 - categories_start) as u16;
1006
1007            self.char_info_buffer.push(CharData {
1008                byte_offset: byte_offset as u32,
1009                is_kanji: is_kanji(c),
1010                categories_start,
1011                categories_len,
1012                kanji_run_byte_len: 0,
1013            });
1014        }
1015        // Sentinel for end of text
1016        self.char_info_buffer.push(CharData {
1017            byte_offset: len as u32,
1018            is_kanji: false,
1019            categories_start: 0,
1020            categories_len: 0,
1021            kanji_run_byte_len: 0,
1022        });
1023
1024        // Pre-calculate Kanji run lengths (backwards)
1025        for i in (0..self.char_info_buffer.len() - 1).rev() {
1026            if self.char_info_buffer[i].is_kanji {
1027                let next_byte_offset = self.char_info_buffer[i + 1].byte_offset;
1028                let char_byte_len = next_byte_offset - self.char_info_buffer[i].byte_offset;
1029                self.char_info_buffer[i].kanji_run_byte_len =
1030                    char_byte_len + self.char_info_buffer[i + 1].kanji_run_byte_len;
1031            } else {
1032                self.char_info_buffer[i].kanji_run_byte_len = 0;
1033            }
1034        }
1035
1036        let start_edge = Edge {
1037            path_cost: 0,
1038            left_index: u16::MAX,
1039            ..Default::default()
1040        };
1041        self.ends_at[0].push(start_edge);
1042
1043        let mut unknown_word_end: Option<usize> = None;
1044
1045        // Pre-scan text with Aho-Corasick
1046        // Buffers are Lattice fields reused across calls; refill matches_head (its
1047        // contents are meaningful, unlike ends_at's empty-Vec slots) and clear
1048        // matches_store (a plain append-only pool).
1049        self.matches_head.clear();
1050        self.matches_head.resize(len + 1, usize::MAX);
1051        self.matches_store.clear();
1052
1053        // System dictionary scan
1054        for m in dict.da.find_overlapping_iter(text) {
1055            let start = m.start();
1056            let (offset, count) = dict.decode_val(m.value());
1057            let offset_bytes = (offset as usize) * WordEntry::SERIALIZED_LEN;
1058
1059            if offset_bytes < dict.vals_data.len() {
1060                let data_slice = &dict.vals_data[offset_bytes..];
1061                for i in 0..count {
1062                    let entry_offset = WordEntry::SERIALIZED_LEN * (i as usize);
1063                    if entry_offset + WordEntry::SERIALIZED_LEN <= data_slice.len() {
1064                        let entry = WordEntry::deserialize(&data_slice[entry_offset..], true);
1065                        if start < self.matches_head.len() {
1066                            let next = self.matches_head[start];
1067                            self.matches_head[start] = self.matches_store.len();
1068                            self.matches_store.push((m.end(), entry, next));
1069                        }
1070                    }
1071                }
1072            }
1073        }
1074
1075        // User dictionary scan
1076        if let Some(ud) = user_dict {
1077            for m in ud.da.find_overlapping_iter(text) {
1078                let start = m.start();
1079                let (offset, count) = ud.decode_val(m.value());
1080                let offset_bytes = (offset as usize) * WordEntry::SERIALIZED_LEN;
1081
1082                if offset_bytes < ud.vals_data.len() {
1083                    let data_slice = &ud.vals_data[offset_bytes..];
1084                    for i in 0..count {
1085                        let entry_offset = WordEntry::SERIALIZED_LEN * (i as usize);
1086                        if entry_offset + WordEntry::SERIALIZED_LEN <= data_slice.len() {
1087                            let entry = WordEntry::deserialize(&data_slice[entry_offset..], false);
1088                            if start < self.matches_head.len() {
1089                                let next = self.matches_head[start];
1090                                self.matches_head[start] = self.matches_store.len();
1091                                self.matches_store.push((m.end(), entry, next));
1092                            }
1093                        }
1094                    }
1095                }
1096            }
1097        }
1098
1099        for char_idx in 0..self.char_info_buffer.len() - 1 {
1100            let start = self.char_info_buffer[char_idx].byte_offset as usize;
1101
1102            if self.ends_at[start].is_empty() {
1103                continue;
1104            }
1105
1106            let mut found: bool = false;
1107
1108            if start < self.matches_head.len() {
1109                let mut match_idx = self.matches_head[start];
1110                while match_idx != usize::MAX {
1111                    let (end, word_entry, next) = self.matches_store[match_idx];
1112
1113                    let prefix_len = end - start;
1114                    let kanji_only = self.is_kanji_all(char_idx, prefix_len);
1115                    let edge = Self::create_edge(word_entry, start, end, kanji_only);
1116                    self.add_edge_in_lattice_nbest(edge, cost_matrix, search_mode);
1117                    found = true;
1118
1119                    match_idx = next;
1120                }
1121            }
1122
1123            if (search_mode.is_search()
1124                || unknown_word_end.map(|index| index <= start).unwrap_or(true))
1125                && char_idx < self.char_info_buffer.len() - 1
1126            {
1127                let num_categories = self.char_info_buffer[char_idx].categories_len as usize;
1128                for category_ord in 0..num_categories {
1129                    let category = self.get_cached_category(char_idx, category_ord);
1130                    unknown_word_end = self.process_unknown_word_nbest(
1131                        char_definitions,
1132                        unknown_dictionary,
1133                        cost_matrix,
1134                        search_mode,
1135                        category,
1136                        category_ord,
1137                        unknown_word_end,
1138                        start,
1139                        char_idx,
1140                        found,
1141                    );
1142                }
1143            }
1144        }
1145
1146        // Connect EOS with all-path recording
1147        if !self.ends_at[len].is_empty() {
1148            let eos_edge_index = self.ends_at[len].len() as u16;
1149            let mut eos_edge = Edge {
1150                start_index: len as u32,
1151                stop_index: len as u32,
1152                ..Default::default()
1153            };
1154            let left_edges = &self.ends_at[len];
1155            let mut best_cost = i32::MAX;
1156            let mut best_left = None;
1157            let right_left_id = 0; // EOS default left_id
1158
1159            for (i, left_edge) in left_edges.iter().enumerate() {
1160                let left_right_id = left_edge.word_entry.right_id();
1161                let conn_cost = cost_matrix.cost(left_right_id, right_left_id);
1162                let path_cost = left_edge.path_cost.saturating_add(conn_cost);
1163
1164                // Record all transitions to EOS
1165                self.all_paths[len].push(PathEntry {
1166                    edge_index: eos_edge_index,
1167                    left_pos: len as u32,
1168                    left_index: i as u16,
1169                    cost: path_cost,
1170                });
1171
1172                if path_cost < best_cost {
1173                    best_cost = path_cost;
1174                    best_left = Some(i as u16);
1175                }
1176            }
1177            if let Some(left_idx) = best_left {
1178                eos_edge.left_index = left_idx;
1179                eos_edge.path_cost = best_cost;
1180                self.ends_at[len].push(eos_edge);
1181            }
1182        }
1183    }
1184
1185    /// Returns the top-N paths through the lattice.
1186    /// Each result is a (path, cost) pair where path is a Vec of (byte_start, WordId) pairs.
1187    /// The first result (index 0) is the 1-best path.
1188    /// If `unique` is true, paths with the same segmentation (same byte_start sequence)
1189    /// are deduplicated, keeping only the first (lowest cost) variant.
1190    /// If `cost_threshold` is Some(t), paths whose cost exceeds best_cost + t are discarded.
1191    /// Requires set_text_nbest() to have been called first.
1192    pub fn nbest_tokens_offset(
1193        &self,
1194        n: usize,
1195        unique: bool,
1196        cost_threshold: Option<i64>,
1197    ) -> Vec<(Vec<(usize, WordId)>, i64)> {
1198        use std::collections::HashSet;
1199
1200        use crate::nbest::NBestGenerator;
1201        let mut generator = NBestGenerator::new(self);
1202        let mut results = Vec::with_capacity(n);
1203        let mut best_cost: Option<i64> = None;
1204
1205        if unique {
1206            let mut seen: HashSet<Vec<usize>> = HashSet::new();
1207            while results.len() < n {
1208                match generator.next() {
1209                    Some((path, cost)) => {
1210                        // Record best cost from first result
1211                        let bc = *best_cost.get_or_insert(cost);
1212                        // Skip if cost exceeds threshold
1213                        if let Some(threshold) = cost_threshold
1214                            && cost > bc + threshold
1215                        {
1216                            break;
1217                        }
1218                        let key: Vec<usize> = path.iter().map(|(start, _)| *start).collect();
1219                        if seen.insert(key) {
1220                            results.push((path, cost));
1221                        }
1222                    }
1223                    None => break,
1224                }
1225            }
1226        } else {
1227            while results.len() < n {
1228                match generator.next() {
1229                    Some((path, cost)) => {
1230                        let bc = *best_cost.get_or_insert(cost);
1231                        if let Some(threshold) = cost_threshold
1232                            && cost > bc + threshold
1233                        {
1234                            break;
1235                        }
1236                        results.push((path, cost));
1237                    }
1238                    None => break,
1239                }
1240            }
1241        }
1242        results
1243    }
1244}
1245
1246#[cfg(test)]
1247mod tests {
1248    use crate::viterbi::{Lattice, LexType, WordEntry, WordId};
1249
1250    #[test]
1251    fn test_word_entry() {
1252        let mut buffer = Vec::new();
1253        let word_entry =
1254            WordEntry::new(WordId::new(LexType::System, 1u32), -17i16, 1411u16, 1412u16);
1255        word_entry.serialize(&mut buffer).unwrap();
1256        assert_eq!(WordEntry::SERIALIZED_LEN, buffer.len());
1257        let word_entry2 = WordEntry::deserialize(&buffer[..], true);
1258        assert_eq!(word_entry, word_entry2);
1259    }
1260
1261    /// Regression test for #827: `Vec::resize` clones its template value
1262    /// into all but the last new slot, and cloning an empty Vec yields
1263    /// capacity 0, so only the last slot was actually pre-sized. Every
1264    /// newly-grown `ends_at` slot must get the intended pre-size, both on
1265    /// the initial growth and on a later, larger growth.
1266    #[test]
1267    fn test_set_capacity_presizes_all_new_slots() {
1268        let mut lattice = Lattice::default();
1269
1270        lattice.set_capacity(5);
1271        assert_eq!(lattice.ends_at.len(), 6);
1272        for (i, slot) in lattice.ends_at.iter().enumerate() {
1273            assert!(
1274                slot.capacity() >= 16,
1275                "slot {} has capacity {} < 16 after initial growth",
1276                i,
1277                slot.capacity()
1278            );
1279        }
1280
1281        // Growing an already-used lattice must pre-size the appended slots too.
1282        lattice.set_capacity(10);
1283        assert_eq!(lattice.ends_at.len(), 11);
1284        for (i, slot) in lattice.ends_at.iter().enumerate() {
1285            assert!(
1286                slot.capacity() >= 16,
1287                "slot {} has capacity {} < 16 after second growth",
1288                i,
1289                slot.capacity()
1290            );
1291        }
1292    }
1293}