use std::io;
use byteorder::{ByteOrder, LittleEndian, WriteBytesExt};
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use serde::{Deserialize, Serialize};
use crate::dictionary::character_definition::{CategoryId, CharacterDefinition};
use crate::dictionary::connection_cost_matrix::ConnectionCostMatrix;
use crate::dictionary::prefix_dictionary::{PrefixDictionary, UserPrefixDictionary};
use crate::dictionary::unknown_dictionary::UnknownDictionary;
use crate::mode::{Mode, Penalty};
use crate::space_penalty::SpacePenaltyTable;
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
Serialize,
Deserialize,
Default,
Archive,
RkyvSerialize,
RkyvDeserialize,
)]
pub enum LexType {
#[default]
System,
User,
Unknown,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
Serialize,
Deserialize,
Archive,
RkyvDeserialize,
RkyvSerialize,
)]
pub struct WordId {
id: u32,
is_system: bool,
lex_type: LexType,
}
impl WordId {
pub fn new(lex_type: LexType, id: u32) -> Self {
WordId {
id,
is_system: matches!(lex_type, LexType::System),
lex_type,
}
}
#[inline]
pub fn id(&self) -> u32 {
self.id
}
#[inline]
pub fn is_unknown(&self) -> bool {
matches!(self.lex_type, LexType::Unknown)
}
#[inline]
pub fn is_system(&self) -> bool {
self.is_system
}
#[inline]
pub fn lex_type(&self) -> LexType {
self.lex_type
}
}
impl Default for WordId {
fn default() -> Self {
WordId {
id: u32::MAX,
is_system: true,
lex_type: LexType::System,
}
}
}
#[derive(
Default,
Clone,
Copy,
Debug,
Eq,
PartialEq,
Serialize,
Deserialize,
Archive,
RkyvSerialize,
RkyvDeserialize,
)]
pub struct WordEntry {
word_id: WordId,
word_cost: i16,
left_id: u16,
right_id: u16,
}
impl WordEntry {
pub(crate) const SERIALIZED_LEN: usize = 10;
#[inline]
pub fn new(word_id: WordId, word_cost: i16, left_id: u16, right_id: u16) -> Self {
WordEntry {
word_id,
word_cost,
left_id,
right_id,
}
}
#[inline]
pub fn word_id(&self) -> WordId {
self.word_id
}
#[inline]
pub fn word_cost(&self) -> i16 {
self.word_cost
}
#[inline]
pub fn left_id(&self) -> u32 {
self.left_id as u32
}
#[inline]
pub fn right_id(&self) -> u32 {
self.right_id as u32
}
pub(crate) fn serialize<W: io::Write>(&self, wtr: &mut W) -> io::Result<()> {
wtr.write_u32::<LittleEndian>(self.word_id.id)?;
wtr.write_i16::<LittleEndian>(self.word_cost)?;
wtr.write_u16::<LittleEndian>(self.left_id)?;
wtr.write_u16::<LittleEndian>(self.right_id)?;
Ok(())
}
pub(crate) fn deserialize(
data: &[u8; Self::SERIALIZED_LEN],
is_system_entry: bool,
) -> WordEntry {
let word_id = WordId::new(
if is_system_entry {
LexType::System
} else {
LexType::User
},
LittleEndian::read_u32(&data[0..4]),
);
let word_cost = LittleEndian::read_i16(&data[4..6]);
let left_id = LittleEndian::read_u16(&data[6..8]);
let right_id = LittleEndian::read_u16(&data[8..10]);
WordEntry {
word_id,
word_cost,
left_id,
right_id,
}
}
}
const EDGE_FLAG_KANJI_ONLY: u8 = 0b100;
const EDGE_LEX_TYPE_MASK: u8 = 0b011;
#[derive(Clone, Debug)]
pub struct Edge {
word_id: u32,
path_cost: i32,
left_index: u16,
left_id: u16,
right_id: u16,
word_cost: i16,
start_char: u16,
flags: u8,
}
impl Default for Edge {
fn default() -> Self {
Edge {
word_id: u32::MAX,
path_cost: 0,
left_index: 0,
left_id: 0,
right_id: 0,
word_cost: 0,
start_char: 0,
flags: 0, }
}
}
impl Edge {
#[inline]
fn pack_flags(lex_type: LexType, kanji_only: bool) -> u8 {
let lex_bits = match lex_type {
LexType::System => 0,
LexType::User => 1,
LexType::Unknown => 2,
};
lex_bits | if kanji_only { EDGE_FLAG_KANJI_ONLY } else { 0 }
}
#[inline]
fn lex_type(&self) -> LexType {
match self.flags & EDGE_LEX_TYPE_MASK {
0 => LexType::System,
1 => LexType::User,
_ => LexType::Unknown,
}
}
#[inline]
pub(crate) fn word_id(&self) -> WordId {
WordId::new(self.lex_type(), self.word_id)
}
#[inline]
pub(crate) fn word_cost(&self) -> i16 {
self.word_cost
}
#[inline]
pub(crate) fn path_cost(&self) -> i32 {
self.path_cost
}
#[inline]
pub(crate) fn left_index(&self) -> u16 {
self.left_index
}
#[inline]
pub(crate) fn start_char(&self) -> u16 {
self.start_char
}
#[inline]
pub(crate) fn kanji_only(&self) -> bool {
self.flags & EDGE_FLAG_KANJI_ONLY != 0
}
}
#[derive(Clone, Debug)]
pub struct PathEntry {
edge_index: u16,
left_pos: u32,
left_index: u16,
cost: i32,
}
impl PathEntry {
#[inline]
pub(crate) fn edge_index(&self) -> u16 {
self.edge_index
}
#[inline]
pub(crate) fn left_pos(&self) -> u32 {
self.left_pos
}
#[inline]
pub(crate) fn left_index(&self) -> u16 {
self.left_index
}
#[inline]
pub(crate) fn cost(&self) -> i32 {
self.cost
}
}
#[derive(Clone, Default)]
pub struct Lattice {
capacity: usize,
ends_at: Vec<Vec<Edge>>,
char_info_buffer: Vec<CharData>,
categories_buffer: Vec<CategoryId>,
all_paths: Vec<Vec<PathEntry>>,
nbest_capacity: usize,
last_n_chars: usize,
max_chars_seen: usize,
matches_head: Vec<u32>,
matches_store: Vec<(u32, WordEntry, u32)>,
chars_buf: Vec<char>,
codes_buf: Vec<u32>,
sys_matches: Vec<(u32, WordEntry)>,
group_runs_buf: Vec<u32>,
group_runs_start_buf: Vec<u32>,
penalty_cache: Vec<i32>,
penalty_cache_pos: usize,
}
const PATH_COST_CLAMP: i32 = i32::MAX - 131_072;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct LatticeOptions<'a> {
pub mode: &'a Mode,
pub max_grouping_len: Option<usize>,
pub unknown_word_ladder: bool,
pub space_penalty: Option<&'a SpacePenaltyTable>,
}
impl<'a> LatticeOptions<'a> {
pub fn new(mode: &'a Mode) -> Self {
Self {
mode,
max_grouping_len: None,
unknown_word_ladder: true,
space_penalty: None,
}
}
#[inline]
fn space_penalty_at(
&self,
char_info: &[CharData],
char_idx: usize,
) -> Option<&'a SpacePenaltyTable> {
let table = self.space_penalty?;
(char_idx > 0 && char_info[char_idx - 1].is_space).then_some(table)
}
}
#[inline]
fn space_penalty_cost(space_penalty: Option<&SpacePenaltyTable>, word_entry: &WordEntry) -> i32 {
space_penalty.map_or(0, |table| table.cost(word_entry.word_id()))
}
const CATEGORIES_IN_POOL: u32 = 1 << 31;
#[derive(Clone, Copy, Debug, Default)]
struct CharData {
byte_offset: u32,
is_kanji: bool,
is_space: bool,
categories_start: u32,
categories_len: u16,
kanji_run_char_len: u32,
}
const _: () = assert!(size_of::<CharData>() == 16);
#[inline]
pub fn is_kanji(c: char) -> bool {
let c = c as u32;
(0x4E00..=0x9FAF).contains(&c) || (0x3400..=0x4DBF).contains(&c)
}
impl Lattice {
#[inline]
fn create_edge(word_entry: WordEntry, start_char: usize, kanji_only: bool) -> Edge {
let word_id = word_entry.word_id();
Edge {
word_id: word_id.id(),
path_cost: i32::MAX,
left_index: u16::MAX,
left_id: word_entry.left_id,
right_id: word_entry.right_id,
word_cost: word_entry.word_cost,
start_char: start_char as u16,
flags: Edge::pack_flags(word_id.lex_type(), kanji_only),
}
}
pub fn clear(&mut self) {
let bound = self.last_n_chars + 1;
for edge_vec in self.ends_at.iter_mut().take(bound) {
edge_vec.clear();
}
debug_assert!(
self.ends_at.iter().skip(bound).all(|v| v.is_empty()),
"ends_at slot beyond last_n_chars must be empty"
);
for path_vec in self.all_paths.iter_mut().take(bound) {
path_vec.clear();
}
debug_assert!(
self.all_paths.iter().skip(bound).all(|v| v.is_empty()),
"all_paths slot beyond last_n_chars must be empty"
);
self.char_info_buffer.clear();
self.categories_buffer.clear();
}
#[inline]
fn is_kanji_all(&self, char_idx: usize, num_chars: usize) -> bool {
self.char_info_buffer[char_idx].kanji_run_char_len >= num_chars as u32
}
#[inline]
fn get_cached_category(
&self,
char_definitions: &CharacterDefinition,
char_idx: usize,
category_ord: usize,
) -> CategoryId {
let char_data = &self.char_info_buffer[char_idx];
let idx = (char_data.categories_start & !CATEGORIES_IN_POOL) as usize + category_ord;
if char_data.categories_start & CATEGORIES_IN_POOL != 0 {
char_definitions.flat_category(idx)
} else {
self.categories_buffer[idx]
}
}
#[inline]
fn char_index_of_byte(&self, byte: usize) -> usize {
self.char_info_buffer
.binary_search_by_key(&(byte as u32), |cd| cd.byte_offset)
.unwrap_or_else(|insert| insert.saturating_sub(1))
}
#[inline]
pub(crate) fn byte_offset_of(&self, char_idx: usize) -> usize {
debug_assert!(
char_idx < self.char_info_buffer.len(),
"char position {char_idx} is outside the prepared sentence"
);
self.char_info_buffer[char_idx].byte_offset as usize
}
fn record_and_grow(&mut self, n_chars: usize) {
self.last_n_chars = n_chars;
self.max_chars_seen = self.max_chars_seen.max(n_chars);
if self.capacity <= n_chars {
self.capacity = n_chars;
self.ends_at
.resize_with(n_chars + 1, || Vec::with_capacity(16));
}
}
fn record_and_grow_nbest(&mut self, n_chars: usize) {
self.record_and_grow(n_chars);
if self.nbest_capacity <= n_chars {
self.nbest_capacity = n_chars;
self.all_paths.resize(n_chars + 1, Vec::new());
}
}
#[cfg(test)]
fn set_capacity(&mut self, n_chars: usize) {
self.clear();
self.record_and_grow(n_chars);
}
#[cfg(test)]
fn set_capacity_nbest(&mut self, n_chars: usize) {
self.clear();
self.record_and_grow_nbest(n_chars);
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn take_max_char_len(&mut self) -> usize {
std::mem::take(&mut self.max_chars_seen)
}
pub fn shrink_to(&mut self, n_chars: usize) {
self.clear();
let slots = n_chars + 1;
if self.capacity > n_chars {
self.ends_at.truncate(slots);
self.ends_at.shrink_to(slots);
for slot in &mut self.ends_at {
slot.shrink_to(16);
}
self.capacity = n_chars;
}
if self.nbest_capacity > n_chars {
self.all_paths.truncate(slots);
self.all_paths.shrink_to(slots);
for paths in &mut self.all_paths {
paths.shrink_to(0);
}
self.nbest_capacity = n_chars;
}
self.last_n_chars = self.last_n_chars.min(n_chars);
self.max_chars_seen = 0;
self.char_info_buffer.shrink_to(slots);
self.categories_buffer.shrink_to(4 * n_chars);
self.matches_head.shrink_to(slots);
self.matches_store.shrink_to(8 * slots);
self.chars_buf.shrink_to(n_chars);
self.codes_buf.shrink_to(n_chars);
self.sys_matches.shrink_to(64);
self.group_runs_buf.shrink_to(4 * n_chars);
self.group_runs_start_buf.shrink_to(slots);
self.penalty_cache.shrink_to(64);
}
fn prepare_char_buffers(
&mut self,
dict: &PrefixDictionary,
char_definitions: &CharacterDefinition,
text: &str,
options: &LatticeOptions,
) {
let len = text.len();
let search_mode = options.mode;
let needs_kanji_runs = search_mode.is_search();
let space_table = options.space_penalty;
self.char_info_buffer.clear();
self.categories_buffer.clear();
self.chars_buf.clear();
self.codes_buf.clear();
self.group_runs_start_buf.clear();
let mut group_runs_total: u32 = 0;
for (byte_offset, c) in text.char_indices() {
let (categories_start, categories_len) =
match char_definitions.lookup_categories_packed(c) {
Some((offset, len)) => (offset | CATEGORIES_IN_POOL, len),
None => {
let start = self.categories_buffer.len() as u32;
for &category in char_definitions.lookup_categories(c) {
self.categories_buffer.push(category);
}
(start, (self.categories_buffer.len() as u32 - start) as u16)
}
};
if needs_kanji_runs {
self.group_runs_start_buf.push(group_runs_total);
group_runs_total += categories_len as u32;
}
let is_space = space_table.is_some_and(|table| table.is_space(c, char_definitions));
self.char_info_buffer.push(CharData {
byte_offset: byte_offset as u32,
is_kanji: needs_kanji_runs && is_kanji(c),
is_space,
categories_start,
categories_len,
kanji_run_char_len: 0,
});
self.chars_buf.push(c);
self.codes_buf.push(dict.map_code_or_invalid(c));
}
self.char_info_buffer.push(CharData {
byte_offset: len as u32,
is_kanji: false,
is_space: false,
categories_start: 0,
categories_len: 0,
kanji_run_char_len: 0,
});
if needs_kanji_runs {
let n_chars = self.char_info_buffer.len() - 1;
for i in (0..n_chars).rev() {
if self.char_info_buffer[i].is_kanji {
self.char_info_buffer[i].kanji_run_char_len =
1 + self.char_info_buffer[i + 1].kanji_run_char_len;
} else {
self.char_info_buffer[i].kanji_run_char_len = 0;
}
}
let mut runs = std::mem::take(&mut self.group_runs_buf);
runs.clear();
runs.resize(group_runs_total as usize, 1);
for i in (0..n_chars).rev() {
let cd = self.char_info_buffer[i];
for ord in 0..cd.categories_len as usize {
let cat = self.get_cached_category(char_definitions, i, ord);
let mut run = 1;
if i + 1 < n_chars {
let next = self.char_info_buffer[i + 1];
if ord < next.categories_len as usize
&& self.get_cached_category(char_definitions, i + 1, ord) == cat
{
run = runs[self.group_runs_start_buf[i + 1] as usize + ord] + 1;
}
}
runs[self.group_runs_start_buf[i] as usize + ord] = run;
}
}
self.group_runs_buf = runs;
}
}
#[allow(clippy::too_many_arguments)]
pub fn set_text(
&mut self,
dict: &PrefixDictionary,
user_dict: &Option<&UserPrefixDictionary>,
char_definitions: &CharacterDefinition,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
text: &str,
search_mode: &Mode,
max_grouping_len: Option<usize>,
unknown_word_ladder: bool,
) {
let mut options = LatticeOptions::new(search_mode);
options.max_grouping_len = max_grouping_len;
options.unknown_word_ladder = unknown_word_ladder;
self.set_text_with_options(
dict,
user_dict,
char_definitions,
unknown_dictionary,
cost_matrix,
text,
&options,
);
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
pub fn set_text_with_options(
&mut self,
dict: &PrefixDictionary,
user_dict: &Option<&UserPrefixDictionary>,
char_definitions: &CharacterDefinition,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
text: &str,
options: &LatticeOptions,
) {
let search_mode = options.mode;
let max_grouping_len = options.max_grouping_len;
let unknown_word_ladder = options.unknown_word_ladder;
self.clear();
self.prepare_char_buffers(dict, char_definitions, text, options);
let n_chars = self.chars_buf.len();
assert!(
n_chars < u16::MAX as usize,
"set_text: sentence has {n_chars} characters, exceeding the u16::MAX-1 limit; \
split the input into sentences (the Segmenter does this automatically)"
);
self.record_and_grow(n_chars);
self.penalty_cache_pos = usize::MAX;
let start_edge = Edge {
path_cost: 0,
left_index: u16::MAX,
..Default::default()
};
self.ends_at[0].push(start_edge);
let mut unknown_word_end: Option<usize> = None;
self.matches_head.clear();
self.matches_store.clear();
if let Some(ud) = user_dict {
self.matches_head.resize(n_chars + 1, u32::MAX);
let ud_vals: &[u8] = &ud.vals_data;
for m in ud.da.find_overlapping_iter(text) {
let start_char = self.char_index_of_byte(m.start());
let (offset, count) = ud.decode_val(m.value());
let offset_bytes = (offset as usize) * WordEntry::SERIALIZED_LEN;
if start_char < self.matches_head.len() {
let avail = ud_vals.len().saturating_sub(offset_bytes);
let n = (count as usize).min(avail / WordEntry::SERIALIZED_LEN);
let block =
&ud_vals[offset_bytes..offset_bytes + n * WordEntry::SERIALIZED_LEN];
let end_char = self.char_index_of_byte(m.end()) as u32;
for chunk in block.as_chunks::<{ WordEntry::SERIALIZED_LEN }>().0 {
let entry = WordEntry::deserialize(chunk, false);
let next = self.matches_head[start_char];
self.matches_head[start_char] = self.matches_store.len() as u32;
self.matches_store.push((end_char, entry, next));
}
}
}
}
for char_idx in 0..n_chars {
if self.ends_at[char_idx].is_empty() {
continue;
}
let mut found: bool = false;
let space_penalty = options.space_penalty_at(&self.char_info_buffer, char_idx);
if char_idx < self.matches_head.len() {
let mut match_idx = self.matches_head[char_idx];
while match_idx != u32::MAX {
let (end_char, word_entry, next) = self.matches_store[match_idx as usize];
let end_char = end_char as usize;
let kanji_only = self.is_kanji_all(char_idx, end_char - char_idx);
let edge = Self::create_edge(word_entry, char_idx, kanji_only);
let extra_cost = space_penalty_cost(space_penalty, &word_entry);
self.add_edge_in_lattice(edge, end_char, cost_matrix, search_mode, extra_cost);
found = true;
match_idx = next;
}
}
self.sys_matches.clear();
{
let suffix = &self.codes_buf[char_idx..];
for (entries, end_char_offset) in dict.common_prefix_search_codes(suffix) {
let end_char = (char_idx + end_char_offset) as u32;
for chunk in entries.as_chunks::<{ WordEntry::SERIALIZED_LEN }>().0 {
self.sys_matches
.push((end_char, WordEntry::deserialize(chunk, true)));
}
}
}
for i in (0..self.sys_matches.len()).rev() {
let (end_char, word_entry) = self.sys_matches[i];
let end_char = end_char as usize;
let kanji_only = self.is_kanji_all(char_idx, end_char - char_idx);
let edge = Self::create_edge(word_entry, char_idx, kanji_only);
let extra_cost = space_penalty_cost(space_penalty, &word_entry);
self.add_edge_in_lattice(edge, end_char, cost_matrix, search_mode, extra_cost);
found = true;
}
if search_mode.is_search()
|| unknown_word_end
.map(|index| index <= char_idx)
.unwrap_or(true)
{
let num_categories = self.char_info_buffer[char_idx].categories_len as usize;
for category_ord in 0..num_categories {
let category =
self.get_cached_category(char_definitions, char_idx, category_ord);
unknown_word_end = self.process_unknown_word(
char_definitions,
unknown_dictionary,
cost_matrix,
search_mode,
max_grouping_len,
unknown_word_ladder,
space_penalty,
category,
category_ord,
unknown_word_end,
char_idx,
found,
);
}
}
}
if !self.ends_at[n_chars].is_empty() {
let mut eos_edge = Edge {
start_char: n_chars as u16,
..Default::default()
};
let left_edges = &self.ends_at[n_chars];
let mut best_cost = i32::MAX;
let mut best_left = None;
let cost_row = cost_matrix.row(0);
for (i, left_edge) in left_edges.iter().enumerate() {
let path_cost = left_edge.path_cost + cost_row[left_edge.right_id as usize] as i32;
let path_cost = match search_mode {
Mode::Normal => path_cost,
Mode::Decompose(penalty) => path_cost.saturating_add(
penalty.penalty(left_edge, n_chars - left_edge.start_char as usize),
),
};
if path_cost < best_cost {
best_cost = path_cost;
best_left = Some(i as u16);
}
}
if let Some(left_idx) = best_left {
eos_edge.left_index = left_idx;
eos_edge.path_cost = best_cost;
self.ends_at[n_chars].push(eos_edge);
}
}
}
#[allow(clippy::too_many_arguments)]
fn emit_unknown_word_edges(
&mut self,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
search_mode: &Mode,
space_penalty: Option<&SpacePenaltyTable>,
category: CategoryId,
char_idx: usize,
num_chars: usize,
) {
let end_char = char_idx + num_chars;
let kanji_only = self.is_kanji_all(char_idx, num_chars);
for &word_id in unknown_dictionary.lookup_word_ids(category) {
let word_entry = unknown_dictionary.word_entry(word_id);
let edge = Self::create_edge(word_entry, char_idx, kanji_only);
let extra_cost = space_penalty_cost(space_penalty, &word_entry);
self.add_edge_in_lattice(edge, end_char, cost_matrix, search_mode, extra_cost);
}
}
#[allow(clippy::too_many_arguments)]
fn emit_unknown_word_edges_nbest(
&mut self,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
search_mode: &Mode,
space_penalty: Option<&SpacePenaltyTable>,
category: CategoryId,
char_idx: usize,
num_chars: usize,
) {
let end_char = char_idx + num_chars;
let kanji_only = self.is_kanji_all(char_idx, num_chars);
for &word_id in unknown_dictionary.lookup_word_ids(category) {
let word_entry = unknown_dictionary.word_entry(word_id);
let edge = Self::create_edge(word_entry, char_idx, kanji_only);
let extra_cost = space_penalty_cost(space_penalty, &word_entry);
self.add_edge_in_lattice_nbest(edge, end_char, cost_matrix, search_mode, extra_cost);
}
}
#[allow(clippy::too_many_arguments)]
fn ladder_run_len(
&self,
char_definitions: &CharacterDefinition,
search_mode: &Mode,
category: CategoryId,
category_ord: usize,
char_idx: usize,
length_cap: usize,
group_run_len: Option<usize>,
) -> usize {
if let Some(run) = group_run_len {
return run.min(length_cap);
}
if search_mode.is_search() {
let run = self.group_runs_buf
[self.group_runs_start_buf[char_idx] as usize + category_ord]
as usize;
return run.min(length_cap);
}
let mut run = 1;
for i in 1..length_cap {
let next_idx = char_idx + i;
if next_idx >= self.char_info_buffer.len() - 1 {
break;
}
let next = self.char_info_buffer[next_idx];
if category_ord >= next.categories_len as usize
|| self.get_cached_category(char_definitions, next_idx, category_ord) != category
{
break;
}
run += 1;
}
run
}
#[allow(clippy::too_many_arguments)]
fn process_unknown_word(
&mut self,
char_definitions: &CharacterDefinition,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
search_mode: &Mode,
max_grouping_len: Option<usize>,
unknown_word_ladder: bool,
space_penalty: Option<&SpacePenaltyTable>,
category: CategoryId,
category_ord: usize,
unknown_word_index: Option<usize>,
char_idx: usize,
found: bool,
) -> Option<usize> {
let mut unknown_word_num_chars: usize = 0;
let mut group_run_len: Option<usize> = None;
let category_data = char_definitions.lookup_definition(category);
if category_data.invoke || !found {
unknown_word_num_chars = 1;
if category_data.group {
let run_len = if search_mode.is_search() {
self.group_runs_buf[self.group_runs_start_buf[char_idx] as usize + category_ord]
as usize
} else {
let mut run = 1;
for i in 1.. {
let next_idx = char_idx + i;
if next_idx >= self.char_info_buffer.len() - 1 {
break;
}
let num_categories =
self.char_info_buffer[next_idx].categories_len as usize;
let mut found_cat = false;
if category_ord < num_categories {
let cat =
self.get_cached_category(char_definitions, next_idx, category_ord);
if cat == category {
run += 1;
found_cat = true;
}
}
if !found_cat {
break;
}
}
run
};
group_run_len = Some(run_len);
unknown_word_num_chars = run_len;
if let Some(cap) = max_grouping_len
&& unknown_word_num_chars > 1
&& unknown_word_num_chars - 1 > cap
{
unknown_word_num_chars = 1;
}
}
}
if unknown_word_num_chars > 0 {
self.emit_unknown_word_edges(
unknown_dictionary,
cost_matrix,
search_mode,
space_penalty,
category,
char_idx,
unknown_word_num_chars,
);
if unknown_word_ladder && category_data.length > 0 {
let length_cap = category_data.length as usize;
let run_len = self.ladder_run_len(
char_definitions,
search_mode,
category,
category_ord,
char_idx,
length_cap,
group_run_len,
);
for i in 1..=run_len {
if i == unknown_word_num_chars {
continue; }
self.emit_unknown_word_edges(
unknown_dictionary,
cost_matrix,
search_mode,
space_penalty,
category,
char_idx,
i,
);
}
}
return Some(char_idx + unknown_word_num_chars);
}
unknown_word_index
}
#[inline(never)]
fn relax_decompose(
&mut self,
start_char: usize,
right_left_id: u32,
penalty: &Penalty,
cost_matrix: &ConnectionCostMatrix,
) -> (i32, Option<u16>) {
let mut cache = std::mem::take(&mut self.penalty_cache);
if self.penalty_cache_pos != start_char {
cache.clear();
cache.extend(self.ends_at[start_char].iter().map(|left_edge| {
penalty.penalty(left_edge, start_char - left_edge.start_char as usize)
}));
self.penalty_cache_pos = start_char;
}
let mut best_cost = i32::MAX;
let mut best_left = None;
let left_edges = &self.ends_at[start_char];
let cost_row = cost_matrix.row(right_left_id);
for (i, left_edge) in left_edges.iter().enumerate() {
let conn_cost = cost_row[left_edge.right_id as usize] as i32;
let total_cost = (left_edge.path_cost + conn_cost).saturating_add(cache[i]);
if total_cost < best_cost {
best_cost = total_cost;
best_left = Some(i as u16);
}
}
self.penalty_cache = cache;
(best_cost, best_left)
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn relax_decompose_nbest(
&mut self,
start_char: usize,
stop_char: usize,
new_edge_index: u16,
right_left_id: u32,
penalty: &Penalty,
cost_matrix: &ConnectionCostMatrix,
extra_cost: i32,
) -> (i32, Option<u16>) {
let mut cache = std::mem::take(&mut self.penalty_cache);
if self.penalty_cache_pos != start_char {
cache.clear();
cache.extend(self.ends_at[start_char].iter().map(|left_edge| {
penalty.penalty(left_edge, start_char - left_edge.start_char as usize)
}));
self.penalty_cache_pos = start_char;
}
let mut best_cost = i32::MAX;
let mut best_left = None;
let cost_row = cost_matrix.row(right_left_id);
for (i, &penalty_cost) in cache.iter().enumerate() {
let left_edge = &self.ends_at[start_char][i];
let conn_cost = cost_row[left_edge.right_id as usize] as i32;
let total_cost = (left_edge.path_cost + conn_cost)
.saturating_add(penalty_cost)
.saturating_add(extra_cost);
self.all_paths[stop_char].push(PathEntry {
edge_index: new_edge_index,
left_pos: start_char as u32,
left_index: i as u16,
cost: total_cost,
});
if total_cost < best_cost {
best_cost = total_cost;
best_left = Some(i as u16);
}
}
self.penalty_cache = cache;
(best_cost, best_left)
}
fn add_edge_in_lattice(
&mut self,
mut edge: Edge,
stop_char: usize,
cost_matrix: &ConnectionCostMatrix,
mode: &Mode,
extra_cost: i32,
) {
let start_char = edge.start_char as usize;
let right_left_id = edge.left_id as u32;
if self.ends_at[start_char].is_empty() {
return;
}
let mut best_cost = i32::MAX;
let mut best_left = None;
match mode {
Mode::Normal => {
let left_edges = &self.ends_at[start_char];
let cost_row = cost_matrix.row(right_left_id);
for (i, left_edge) in left_edges.iter().enumerate() {
let conn_cost = cost_row[left_edge.right_id as usize] as i32;
let total_cost = left_edge.path_cost + conn_cost;
if total_cost < best_cost {
best_cost = total_cost;
best_left = Some(i as u16);
}
}
}
Mode::Decompose(penalty) => {
(best_cost, best_left) =
self.relax_decompose(start_char, right_left_id, penalty, cost_matrix);
}
}
if let Some(best_left_idx) = best_left {
edge.path_cost = best_cost
.saturating_add(extra_cost)
.saturating_add(edge.word_cost as i32)
.min(PATH_COST_CLAMP);
edge.left_index = best_left_idx;
self.ends_at[stop_char].push(edge);
}
}
pub fn tokens_offset(&self) -> Vec<(usize, WordId)> {
let mut offsets = Vec::new();
self.tokens_offset_into(&mut offsets);
offsets
}
pub fn tokens_offset_into(&self, offsets: &mut Vec<(usize, WordId)>) {
offsets.clear();
if self.ends_at.is_empty() {
return;
}
let mut last_idx = self.last_n_chars.min(self.ends_at.len() - 1);
while last_idx > 0 && self.ends_at[last_idx].is_empty() {
last_idx -= 1;
}
if self.ends_at[last_idx].is_empty() {
return;
}
let idx = self.ends_at[last_idx].len() - 1;
let mut edge = &self.ends_at[last_idx][idx];
if edge.left_index == u16::MAX {
return;
}
loop {
if edge.left_index == u16::MAX {
break;
}
let start_char = edge.start_char as usize;
offsets.push((self.byte_offset_of(start_char), edge.word_id()));
edge = &self.ends_at[start_char][edge.left_index as usize];
}
offsets.reverse();
offsets.pop(); }
pub fn char_len(&self) -> usize {
self.last_n_chars
}
pub fn edges_at_char(&self, char_pos: usize) -> &[Edge] {
&self.ends_at[char_pos]
}
pub fn paths_at_char(&self, char_pos: usize) -> &[PathEntry] {
if char_pos < self.all_paths.len() {
&self.all_paths[char_pos]
} else {
&[]
}
}
fn add_edge_in_lattice_nbest(
&mut self,
mut edge: Edge,
stop_char: usize,
cost_matrix: &ConnectionCostMatrix,
mode: &Mode,
extra_cost: i32,
) {
let start_char = edge.start_char as usize;
let right_left_id = edge.left_id as u32;
if self.ends_at[start_char].is_empty() {
return;
}
let mut best_cost = i32::MAX;
let mut best_left = None;
let new_edge_index = self.ends_at[stop_char].len() as u16;
match mode {
Mode::Normal => {
let cost_row = cost_matrix.row(right_left_id);
for i in 0..self.ends_at[start_char].len() {
let left_edge = &self.ends_at[start_char][i];
let total_cost = (left_edge.path_cost
+ cost_row[left_edge.right_id as usize] as i32)
.saturating_add(extra_cost);
self.all_paths[stop_char].push(PathEntry {
edge_index: new_edge_index,
left_pos: start_char as u32,
left_index: i as u16,
cost: total_cost,
});
if total_cost < best_cost {
best_cost = total_cost;
best_left = Some(i as u16);
}
}
}
Mode::Decompose(penalty) => {
(best_cost, best_left) = self.relax_decompose_nbest(
start_char,
stop_char,
new_edge_index,
right_left_id,
penalty,
cost_matrix,
extra_cost,
);
}
}
if let Some(best_left_idx) = best_left {
edge.path_cost = best_cost
.saturating_add(edge.word_cost as i32)
.min(PATH_COST_CLAMP);
edge.left_index = best_left_idx;
self.ends_at[stop_char].push(edge);
}
}
#[allow(clippy::too_many_arguments)]
fn process_unknown_word_nbest(
&mut self,
char_definitions: &CharacterDefinition,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
search_mode: &Mode,
max_grouping_len: Option<usize>,
unknown_word_ladder: bool,
space_penalty: Option<&SpacePenaltyTable>,
category: CategoryId,
category_ord: usize,
unknown_word_index: Option<usize>,
char_idx: usize,
found: bool,
) -> Option<usize> {
let mut unknown_word_num_chars: usize = 0;
let mut group_run_len: Option<usize> = None;
let category_data = char_definitions.lookup_definition(category);
if category_data.invoke || !found {
unknown_word_num_chars = 1;
if category_data.group {
let run_len = if search_mode.is_search() {
self.group_runs_buf[self.group_runs_start_buf[char_idx] as usize + category_ord]
as usize
} else {
let mut run = 1;
for i in 1.. {
let next_idx = char_idx + i;
if next_idx >= self.char_info_buffer.len() - 1 {
break;
}
let num_categories =
self.char_info_buffer[next_idx].categories_len as usize;
let mut found_cat = false;
if category_ord < num_categories {
let cat =
self.get_cached_category(char_definitions, next_idx, category_ord);
if cat == category {
run += 1;
found_cat = true;
}
}
if !found_cat {
break;
}
}
run
};
group_run_len = Some(run_len);
unknown_word_num_chars = run_len;
if let Some(cap) = max_grouping_len
&& unknown_word_num_chars > 1
&& unknown_word_num_chars - 1 > cap
{
unknown_word_num_chars = 1;
}
}
}
if unknown_word_num_chars > 0 {
self.emit_unknown_word_edges_nbest(
unknown_dictionary,
cost_matrix,
search_mode,
space_penalty,
category,
char_idx,
unknown_word_num_chars,
);
if unknown_word_ladder && category_data.length > 0 {
let length_cap = category_data.length as usize;
let run_len = self.ladder_run_len(
char_definitions,
search_mode,
category,
category_ord,
char_idx,
length_cap,
group_run_len,
);
for i in 1..=run_len {
if i == unknown_word_num_chars {
continue;
}
self.emit_unknown_word_edges_nbest(
unknown_dictionary,
cost_matrix,
search_mode,
space_penalty,
category,
char_idx,
i,
);
}
}
return Some(char_idx + unknown_word_num_chars);
}
unknown_word_index
}
#[allow(clippy::too_many_arguments)]
pub fn set_text_nbest(
&mut self,
dict: &PrefixDictionary,
user_dict: &Option<&UserPrefixDictionary>,
char_definitions: &CharacterDefinition,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
text: &str,
search_mode: &Mode,
max_grouping_len: Option<usize>,
unknown_word_ladder: bool,
) {
let mut options = LatticeOptions::new(search_mode);
options.max_grouping_len = max_grouping_len;
options.unknown_word_ladder = unknown_word_ladder;
self.set_text_nbest_with_options(
dict,
user_dict,
char_definitions,
unknown_dictionary,
cost_matrix,
text,
&options,
);
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
pub fn set_text_nbest_with_options(
&mut self,
dict: &PrefixDictionary,
user_dict: &Option<&UserPrefixDictionary>,
char_definitions: &CharacterDefinition,
unknown_dictionary: &UnknownDictionary,
cost_matrix: &ConnectionCostMatrix,
text: &str,
options: &LatticeOptions,
) {
let search_mode = options.mode;
let max_grouping_len = options.max_grouping_len;
let unknown_word_ladder = options.unknown_word_ladder;
self.clear();
self.prepare_char_buffers(dict, char_definitions, text, options);
let n_chars = self.chars_buf.len();
assert!(
n_chars < u16::MAX as usize,
"set_text_nbest: sentence has {n_chars} characters, exceeding the u16::MAX-1 limit; \
split the input into sentences (the Segmenter does this automatically)"
);
self.record_and_grow_nbest(n_chars);
self.penalty_cache_pos = usize::MAX;
let start_edge = Edge {
path_cost: 0,
left_index: u16::MAX,
..Default::default()
};
self.ends_at[0].push(start_edge);
let mut unknown_word_end: Option<usize> = None;
self.matches_head.clear();
self.matches_store.clear();
if let Some(ud) = user_dict {
self.matches_head.resize(n_chars + 1, u32::MAX);
let ud_vals: &[u8] = &ud.vals_data;
for m in ud.da.find_overlapping_iter(text) {
let start_char = self.char_index_of_byte(m.start());
let (offset, count) = ud.decode_val(m.value());
let offset_bytes = (offset as usize) * WordEntry::SERIALIZED_LEN;
if start_char < self.matches_head.len() {
let avail = ud_vals.len().saturating_sub(offset_bytes);
let n = (count as usize).min(avail / WordEntry::SERIALIZED_LEN);
let block =
&ud_vals[offset_bytes..offset_bytes + n * WordEntry::SERIALIZED_LEN];
let end_char = self.char_index_of_byte(m.end()) as u32;
for chunk in block.as_chunks::<{ WordEntry::SERIALIZED_LEN }>().0 {
let entry = WordEntry::deserialize(chunk, false);
let next = self.matches_head[start_char];
self.matches_head[start_char] = self.matches_store.len() as u32;
self.matches_store.push((end_char, entry, next));
}
}
}
}
for char_idx in 0..n_chars {
if self.ends_at[char_idx].is_empty() {
continue;
}
let mut found: bool = false;
let space_penalty = options.space_penalty_at(&self.char_info_buffer, char_idx);
if char_idx < self.matches_head.len() {
let mut match_idx = self.matches_head[char_idx];
while match_idx != u32::MAX {
let (end_char, word_entry, next) = self.matches_store[match_idx as usize];
let end_char = end_char as usize;
let kanji_only = self.is_kanji_all(char_idx, end_char - char_idx);
let edge = Self::create_edge(word_entry, char_idx, kanji_only);
let extra_cost = space_penalty_cost(space_penalty, &word_entry);
self.add_edge_in_lattice_nbest(
edge,
end_char,
cost_matrix,
search_mode,
extra_cost,
);
found = true;
match_idx = next;
}
}
self.sys_matches.clear();
{
let suffix = &self.codes_buf[char_idx..];
for (entries, end_char_offset) in dict.common_prefix_search_codes(suffix) {
let end_char = (char_idx + end_char_offset) as u32;
for chunk in entries.as_chunks::<{ WordEntry::SERIALIZED_LEN }>().0 {
self.sys_matches
.push((end_char, WordEntry::deserialize(chunk, true)));
}
}
}
for i in (0..self.sys_matches.len()).rev() {
let (end_char, word_entry) = self.sys_matches[i];
let end_char = end_char as usize;
let kanji_only = self.is_kanji_all(char_idx, end_char - char_idx);
let edge = Self::create_edge(word_entry, char_idx, kanji_only);
let extra_cost = space_penalty_cost(space_penalty, &word_entry);
self.add_edge_in_lattice_nbest(
edge,
end_char,
cost_matrix,
search_mode,
extra_cost,
);
found = true;
}
if search_mode.is_search()
|| unknown_word_end
.map(|index| index <= char_idx)
.unwrap_or(true)
{
let num_categories = self.char_info_buffer[char_idx].categories_len as usize;
for category_ord in 0..num_categories {
let category =
self.get_cached_category(char_definitions, char_idx, category_ord);
unknown_word_end = self.process_unknown_word_nbest(
char_definitions,
unknown_dictionary,
cost_matrix,
search_mode,
max_grouping_len,
unknown_word_ladder,
space_penalty,
category,
category_ord,
unknown_word_end,
char_idx,
found,
);
}
}
}
if !self.ends_at[n_chars].is_empty() {
let eos_edge_index = self.ends_at[n_chars].len() as u16;
let mut eos_edge = Edge {
start_char: n_chars as u16,
..Default::default()
};
let mut best_cost = i32::MAX;
let mut best_left = None;
let cost_row = cost_matrix.row(0);
for i in 0..self.ends_at[n_chars].len() {
let left_edge = &self.ends_at[n_chars][i];
let path_cost = left_edge.path_cost + cost_row[left_edge.right_id as usize] as i32;
let path_cost = match search_mode {
Mode::Normal => path_cost,
Mode::Decompose(penalty) => path_cost.saturating_add(
penalty.penalty(left_edge, n_chars - left_edge.start_char as usize),
),
};
self.all_paths[n_chars].push(PathEntry {
edge_index: eos_edge_index,
left_pos: n_chars as u32,
left_index: i as u16,
cost: path_cost,
});
if path_cost < best_cost {
best_cost = path_cost;
best_left = Some(i as u16);
}
}
if let Some(left_idx) = best_left {
eos_edge.left_index = left_idx;
eos_edge.path_cost = best_cost;
self.ends_at[n_chars].push(eos_edge);
}
}
}
pub fn nbest_tokens_offset(
&self,
n: usize,
unique: bool,
cost_threshold: Option<i64>,
) -> Vec<(Vec<(usize, WordId)>, i64)> {
use std::collections::HashSet;
use crate::nbest::NBestGenerator;
let mut generator = NBestGenerator::new(self);
let mut results = Vec::with_capacity(n);
let mut best_cost: Option<i64> = None;
if unique {
let mut seen: HashSet<Vec<usize>> = HashSet::new();
while results.len() < n {
match generator.next() {
Some((path, cost)) => {
let bc = *best_cost.get_or_insert(cost);
if let Some(threshold) = cost_threshold
&& cost > bc + threshold
{
break;
}
let key: Vec<usize> = path.iter().map(|(start, _)| *start).collect();
if seen.insert(key) {
results.push((path, cost));
}
}
None => break,
}
}
} else {
while results.len() < n {
match generator.next() {
Some((path, cost)) => {
let bc = *best_cost.get_or_insert(cost);
if let Some(threshold) = cost_threshold
&& cost > bc + threshold
{
break;
}
results.push((path, cost));
}
None => break,
}
}
}
results
}
}
#[cfg(test)]
mod tests {
use crate::viterbi::{CharData, Edge, Lattice, LatticeOptions, LexType, WordEntry, WordId};
fn test_edge(word_id: u32, start_char: usize, left_index: u16) -> Edge {
let mut edge = Lattice::create_edge(
WordEntry::new(WordId::new(LexType::System, word_id), 0, 0, 0),
start_char,
false,
);
edge.left_index = left_index;
edge.path_cost = 0;
edge
}
fn seed_identity_chars(lattice: &mut Lattice, n_chars: usize) {
lattice.char_info_buffer.clear();
for i in 0..=n_chars {
lattice.char_info_buffer.push(CharData {
byte_offset: i as u32,
..Default::default()
});
}
}
#[test]
fn test_group_runs_match_forward_scan() {
use std::collections::BTreeMap;
use crate::dictionary::character_definition::{
CategoryData, CategoryId, CharacterDefinition, LookupTable,
};
use crate::dictionary::prefix_dictionary::PrefixDictionary;
use crate::mode::{Mode, Penalty};
let mapping = LookupTable::from_fn(vec![0u32, 0x61, 0x64, 0x67], &|c,
buf: &mut Vec<
CategoryId,
>| {
if (0x61..0x64).contains(&c) {
buf.push(CategoryId(0));
} else if (0x64..0x67).contains(&c) {
buf.push(CategoryId(1));
buf.push(CategoryId(0));
} else {
buf.push(CategoryId(2));
}
});
let defs = vec![
CategoryData {
invoke: true,
group: true,
length: 0,
};
3
];
let names = vec!["A".into(), "B".into(), "C".into()];
let chardef = CharacterDefinition::new(defs, names, mapping);
let mut map = BTreeMap::new();
map.insert(
"x".to_string(),
vec![WordEntry::new(WordId::new(LexType::System, 0), 0, 0, 0)],
);
let dict = PrefixDictionary::from_word_entry_map(&map).unwrap();
let mut lattice = Lattice::default();
let mode = Mode::Decompose(Penalty::default());
let text = "aabdddefzza";
lattice.prepare_char_buffers(&dict, &chardef, text, &LatticeOptions::new(&mode));
let n_chars = lattice.chars_buf.len();
assert_eq!(n_chars, text.chars().count());
for i in 0..n_chars {
let char_data = lattice.char_info_buffer[i];
for ord in 0..char_data.categories_len as usize {
let cat = lattice.get_cached_category(&chardef, i, ord);
let mut expected = 1usize;
for j in i + 1..n_chars {
let next = lattice.char_info_buffer[j];
if ord < next.categories_len as usize
&& lattice.get_cached_category(&chardef, j, ord) == cat
{
expected += 1;
} else {
break;
}
}
assert_eq!(
lattice.group_runs_buf[lattice.group_runs_start_buf[i] as usize + ord] as usize,
expected,
"run mismatch at char {i} ordinal {ord}"
);
}
}
}
#[test]
fn test_word_entry() {
let mut buffer = Vec::new();
let word_entry =
WordEntry::new(WordId::new(LexType::System, 1u32), -17i16, 1411u16, 1412u16);
word_entry.serialize(&mut buffer).unwrap();
assert_eq!(WordEntry::SERIALIZED_LEN, buffer.len());
let bytes: &[u8; WordEntry::SERIALIZED_LEN] = buffer.as_slice().try_into().unwrap();
let word_entry2 = WordEntry::deserialize(bytes, true);
assert_eq!(word_entry, word_entry2);
}
#[test]
fn test_set_capacity_presizes_all_new_slots() {
let mut lattice = Lattice::default();
lattice.set_capacity(5);
assert_eq!(lattice.ends_at.len(), 6);
for (i, slot) in lattice.ends_at.iter().enumerate() {
assert!(
slot.capacity() >= 16,
"slot {} has capacity {} < 16 after initial growth",
i,
slot.capacity()
);
}
lattice.set_capacity(10);
assert_eq!(lattice.ends_at.len(), 11);
for (i, slot) in lattice.ends_at.iter().enumerate() {
assert!(
slot.capacity() >= 16,
"slot {} has capacity {} < 16 after second growth",
i,
slot.capacity()
);
}
}
#[test]
fn test_clear_after_shrink_leaves_no_stale_edges() {
let mut lattice = Lattice::default();
lattice.set_capacity(100);
lattice.ends_at[0].push(test_edge(1, 0, u16::MAX));
lattice.ends_at[57].push(test_edge(2, 0, 0));
lattice.ends_at[100].push(test_edge(3, 57, 0));
lattice.set_capacity(10);
assert!(
lattice.ends_at.iter().all(|v| v.is_empty()),
"stale edges survived a bounded clear"
);
lattice.ends_at[10].push(test_edge(4, 0, 0)); lattice.set_capacity(3);
assert!(
lattice.ends_at.iter().all(|v| v.is_empty()),
"stale edge at the previous boundary slot survived"
);
}
#[test]
fn test_tokens_offset_finds_eos_at_last_text_len_after_shrink() {
let mut lattice = Lattice::default();
lattice.set_capacity(100);
lattice.set_capacity(3);
seed_identity_chars(&mut lattice, 3);
lattice.ends_at[0].push(test_edge(0, 0, u16::MAX)); lattice.ends_at[3].push(test_edge(42, 0, 0)); lattice.ends_at[3].push(test_edge(0, 3, 0));
let offsets = lattice.tokens_offset();
assert_eq!(offsets.len(), 1);
assert_eq!(offsets[0].0, 0);
assert_eq!(offsets[0].1, WordId::new(LexType::System, 42));
}
#[test]
fn test_shrink_to_truncates_and_keeps_presize() {
let mut lattice = Lattice::default();
lattice.set_capacity(100);
lattice.ends_at[0].push(test_edge(1, 0, u16::MAX));
lattice.ends_at[100].push(test_edge(2, 0, 0));
lattice.shrink_to(10);
assert_eq!(lattice.capacity(), 10);
assert_eq!(lattice.ends_at.len(), 11);
assert!(
lattice.ends_at.iter().all(|v| v.is_empty()),
"shrink_to must clear all slots"
);
for (i, slot) in lattice.ends_at.iter().enumerate() {
assert!(
slot.capacity() >= 16,
"slot {} lost its pre-size after shrink_to (capacity {})",
i,
slot.capacity()
);
}
lattice.set_capacity(50);
assert_eq!(lattice.ends_at.len(), 51);
for (i, slot) in lattice.ends_at.iter().enumerate() {
assert!(
slot.capacity() >= 16,
"slot {} not pre-sized after regrowth (capacity {})",
i,
slot.capacity()
);
}
}
#[test]
fn test_shrink_to_noop_when_target_not_smaller() {
let mut lattice = Lattice::default();
lattice.set_capacity(5);
lattice.shrink_to(100);
assert_eq!(lattice.capacity(), 5);
assert_eq!(lattice.ends_at.len(), 6);
lattice.shrink_to(5);
assert_eq!(lattice.capacity(), 5);
assert_eq!(lattice.ends_at.len(), 6);
let mut fresh = Lattice::default();
fresh.shrink_to(0);
assert_eq!(fresh.capacity(), 0);
assert!(fresh.ends_at.is_empty());
}
#[test]
fn test_backtrace_works_after_shrink_to() {
let mut lattice = Lattice::default();
lattice.set_capacity(100);
lattice.ends_at[0].push(test_edge(1, 0, u16::MAX));
lattice.ends_at[100].push(test_edge(2, 0, 0));
lattice.shrink_to(10);
lattice.set_capacity(3);
seed_identity_chars(&mut lattice, 3);
lattice.ends_at[0].push(test_edge(0, 0, u16::MAX)); lattice.ends_at[3].push(test_edge(42, 0, 0)); lattice.ends_at[3].push(test_edge(0, 3, 0));
let offsets = lattice.tokens_offset();
assert_eq!(offsets.len(), 1);
assert_eq!(offsets[0].0, 0);
assert_eq!(offsets[0].1, WordId::new(LexType::System, 42));
lattice.clear();
assert!(lattice.ends_at.iter().all(|v| v.is_empty()));
}
#[test]
fn test_shrink_to_releases_nbest_paths() {
let mut lattice = Lattice::default();
lattice.set_capacity_nbest(100);
assert_eq!(lattice.all_paths.len(), 101);
lattice.shrink_to(10);
assert_eq!(lattice.all_paths.len(), 11);
assert_eq!(lattice.nbest_capacity, 10);
assert!(lattice.all_paths.iter().all(|v| v.is_empty()));
lattice.set_capacity_nbest(20);
assert_eq!(lattice.all_paths.len(), 21);
}
#[test]
fn test_tokens_offset_into_matches_tokens_offset() {
let mut lattice = Lattice::default();
lattice.set_capacity(3);
seed_identity_chars(&mut lattice, 3);
lattice.ends_at[0].push(test_edge(0, 0, u16::MAX)); lattice.ends_at[3].push(test_edge(7, 0, 0)); lattice.ends_at[3].push(test_edge(0, 3, 0));
let mut reused = vec![(999usize, WordId::default())]; lattice.tokens_offset_into(&mut reused);
assert_eq!(reused, lattice.tokens_offset());
assert_eq!(reused.len(), 1);
lattice.clear();
lattice.tokens_offset_into(&mut reused);
assert!(reused.is_empty());
assert!(lattice.tokens_offset().is_empty());
}
}