msime-engine 0.1.1

Input method engine for Chinese and other languages: quanpin, shuangpin, nine-key, wubi, zhuyin, Cantonese, Japanese, Korean and more, ranked over a word lattice with typo correction and learning from what the user picks. The engine of 水杉输入法 (Metasequoia IME).
Documentation
use super::super::decoder::test_model;
use super::super::romaji::{convert_romaji, kana_for_romaji_prefix};
use super::*;

#[test]
fn pending_exact_completions_share_one_reading_key() {
    let dictionary = JapaneseDictionary::from_bytes(
        test_model::bytes(
            &[("かか", "仮仮", 0, 0, 500), ("かき", "仮木", 0, 0, 600)],
            1,
            &[0],
        )
        .into_boxed_slice(),
    )
    .expect("合成词库");
    let conversion = convert_romaji("kak");
    let expected = reference_search(&dictionary, &conversion, 12);
    let _ = search_converted(&dictionary, &conversion, 12);
    let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
        search_converted(&dictionary, &conversion, 12)
    });
    assert_eq!(actual, expected);
    eprintln!("日文待定精确补全矩阵分配:{allocations}");
    assert!(
        allocations <= 13,
        "精确补全应复用读音键,继续补全词条应直接消费:{allocations}"
    );
}

// 固定基线 dbfdd576e 的批量构造、稳定排序及截断流程,独立对照提前筛选的行为。
fn reference_search(
    dictionary: &JapaneseDictionary,
    conversion: &RomajiConversion,
    limit: usize,
) -> Vec<JapaneseConversion> {
    let reading = conversion.hiragana.as_str();
    let pending = conversion.pending.as_str();
    let mut output = Output {
        items: Vec::with_capacity(limit),
        limit,
    };
    if limit == 0 {
        return output.items;
    }
    let pending_kana = kana_for_romaji_prefix(pending);
    if reading.is_empty() {
        for kana in &pending_kana {
            for lemma in dictionary.prefix_lemma_views(kana, 24) {
                output.push(lemma.surface, i64::from(lemma.word_cost));
                if output.full() {
                    return output.items;
                }
            }
        }
        return output.items;
    }

    let boundaries: Vec<usize> = reading
        .char_indices()
        .map(|(index, _)| index)
        .chain(std::iter::once(reading.len()))
        .collect();
    let mora_count = boundaries.len() - 1;
    let mut rows: Vec<Vec<Node>> = (0..=mora_count)
        .map(|_| Vec::with_capacity(MAX_NODES_PER_ROW))
        .collect();
    rows[0].push(Node {
        text: String::new(),
        cost: 0,
        right_id: 0,
    });

    for start in 0..mora_count {
        if rows[start].is_empty() {
            continue;
        }
        let previous_row = std::mem::take(&mut rows[start]);
        let start_byte = boundaries[start];
        let max_end = mora_count.min(start + MAX_LEMMA_MORA);
        for end in start + 1..=max_end {
            let key = &reading[start_byte..boundaries[end]];
            for lemma in dictionary.exact_lemma_views(key, 24) {
                for previous in &previous_row {
                    let cost = previous.cost
                        + i64::from(lemma.word_cost)
                        + i64::from(dictionary.connection_cost(previous.right_id, lemma.left_id));
                    rows[end].push(Node {
                        text: join_text(&previous.text, lemma.surface),
                        cost,
                        right_id: lemma.right_id,
                    });
                }
            }
        }

        let kana = &reading[start_byte..boundaries[start + 1]];
        for previous in &previous_row {
            rows[start + 1].push(Node {
                text: join_text(&previous.text, kana),
                cost: previous.cost + i64::from(UNKNOWN_KANA_COST),
                right_id: 0,
            });
        }

        for row in &mut rows[start + 1..=max_end] {
            reference_keep_best(row);
        }
        rows[start] = previous_row;
    }

    let mut finals = std::mem::take(&mut rows[mora_count]);
    for node in &mut finals {
        node.cost += i64::from(dictionary.connection_cost(node.right_id, 0));
    }
    finals.sort_by_key(|node| node.cost);
    if let Some(best) = finals.first() {
        output.push(&best.text, best.cost);
    }

    if !pending.is_empty() {
        for kana in &pending_kana {
            for lemma in dictionary.exact_lemma_views(&join_text(reading, kana), 16) {
                output.push(lemma.surface, i64::from(lemma.word_cost));
            }
        }
        for lemma in dictionary.continuing_lemma_views(reading, &pending_kana, 48) {
            output.push(lemma.surface, i64::from(lemma.word_cost));
        }
    }

    for node in finals.iter().skip(1) {
        output.push(&node.text, node.cost);
    }

    for end in (1..=mora_count).rev() {
        if output.full() {
            break;
        }
        for lemma in dictionary.exact_lemma_views(&reading[..boundaries[end]], 16) {
            output.push(lemma.surface, i64::from(lemma.word_cost));
            if output.full() {
                break;
            }
        }
    }
    output.items
}

fn reference_keep_best(row: &mut Vec<Node>) {
    if row.len() > MAX_NODES_PER_ROW {
        row.sort_by_key(|node| node.cost);
        row.truncate(MAX_NODES_PER_ROW);
    }
}

#[test]
fn dense_matrix_search_matches_batch_pruning_reference() {
    for seed in 0..32 {
        let readings = ["か", "かか", "かかか", "かき", "かし", "き", "きか", "し"];
        let surfaces: Vec<_> = (0..readings.len() * 27)
            .map(|index| format!("語{}", if seed % 3 == 0 { index % 5 } else { index }))
            .collect();
        let entries: Vec<_> = readings
            .iter()
            .enumerate()
            .flat_map(|(reading_index, reading)| {
                let surfaces = &surfaces;
                (0..1 + seed % 27).map(move |index| {
                    (
                        *reading,
                        surfaces[reading_index * 27 + index].as_str(),
                        ((index + seed) % 3) as u16,
                        ((index + reading_index) % 3) as u16,
                        ((index + reading_index + seed) % 5) as i32 * 20 - 40,
                    )
                })
            })
            .collect();
        let dictionary = JapaneseDictionary::from_bytes(
            test_model::bytes(&entries, 3, &[-15, 0, 40, 8, -5, 2, 90, 11, -30]).into_boxed_slice(),
        )
        .expect("合成词库加载");
        for input in [
            "",
            "ka",
            "kaka",
            "kakaka",
            "kak",
            "kik",
            "kaki",
            "kakashik",
            "ki",
            "q",
            "kiki",
            "kakashishi",
            "kakakakakakakakakakakakakakakakakaka",
        ] {
            let conversion = convert_romaji(input);
            for limit in [0, 1, 2, 8, 12, 16, 32] {
                assert_eq!(
                    search_converted(&dictionary, &conversion, limit),
                    reference_search(&dictionary, &conversion, limit),
                    "合成种子 {seed},读音长度 {},限额 {limit}",
                    input.len()
                );
            }
        }
    }
}

fn node_fields(nodes: &[Node]) -> Vec<(&str, i64, u16)> {
    nodes
        .iter()
        .map(|node| (node.text.as_str(), node.cost, node.right_id))
        .collect()
}

#[test]
fn a_row_keeps_insertion_order_until_the_ninth_node_even_when_rejected() {
    let mut row = Row::new();
    let costs = [9, 1, 5, 2, 5, -1, 4, 8];
    for (index, cost) in costs.into_iter().enumerate() {
        row.extend("合成", &index.to_string(), cost, index as u16);
    }
    assert_eq!(
        row.nodes.iter().map(|node| node.cost).collect::<Vec<_>>(),
        costs
    );
    let capacity = row.nodes.capacity();
    let pointer = row.nodes.as_ptr();
    row.extend("败选", "文本", 100, 9);
    assert_eq!(
        row.nodes
            .iter()
            .map(|node| node.right_id)
            .collect::<Vec<_>>(),
        [5, 1, 3, 6, 2, 4, 7, 0]
    );
    assert_eq!(row.nodes.as_ptr(), pointer);
    assert_eq!(row.nodes.capacity(), capacity);
    let (_, allocations) = crate::ime::personal_rerank::allocations::count(|| {
        row.extend("同分", "败选", 9, 10);
        row.extend("高分", "败选", 100, 11);
    });
    assert_eq!(allocations, 0);
    row.extend("改善", "同分", 5, 12);
    assert_eq!(
        row.nodes
            .iter()
            .map(|node| node.right_id)
            .collect::<Vec<_>>(),
        [5, 1, 3, 6, 2, 4, 12, 7]
    );
}

#[test]
fn rows_match_stable_batch_sort_across_random_batches_without_growing() {
    for seed in 0..32_u64 {
        let mut state = seed + 1;
        let mut row = Row::new();
        let mut reference = Vec::new();
        let pointer = row.nodes.as_ptr();
        let capacity = row.nodes.capacity();
        let mut serial = 0;
        for batch in 0..24 {
            state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
            let count = if batch == 0 {
                seed as usize % 10
            } else {
                (state >> 32) as usize % 193
            };
            for _ in 0..count {
                state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
                let cost = ((state >> 32) % 61) as i64 - 30;
                let suffix = serial.to_string();
                let right_id = (serial % 3) as u16;
                row.extend("合成", &suffix, cost, right_id);
                reference.push(Node {
                    text: join_text("合成", &suffix),
                    cost,
                    right_id,
                });
                serial += 1;
            }
            reference_keep_best(&mut reference);
            assert_eq!(
                node_fields(&row.nodes),
                node_fields(&reference),
                "种子 {seed} 批次 {batch}"
            );
            assert!(row.nodes.len() <= MAX_NODES_PER_ROW);
            assert_eq!(row.nodes.as_ptr(), pointer);
            assert_eq!(row.nodes.capacity(), capacity);
        }
    }
}

#[test]
fn dense_two_mora_search_avoids_losing_cartesian_product_texts() {
    let singles: Vec<_> = (0..24).map(|index| format!("単{index:02}")).collect();
    let doubles: Vec<_> = (0..24).map(|index| format!("双{index:02}")).collect();
    let entries: Vec<_> = singles
        .iter()
        .map(|surface| ("か", surface.as_str(), 0, 0, 0))
        .chain(
            doubles
                .iter()
                .map(|surface| ("かか", surface.as_str(), 0, 0, 0)),
        )
        .collect();
    let dictionary =
        JapaneseDictionary::from_bytes(test_model::bytes(&entries, 1, &[0]).into_boxed_slice())
            .expect("合成词库加载");
    let conversion = convert_romaji("kaka");
    let (reference, baseline_allocations) = crate::ime::personal_rerank::allocations::count(|| {
        reference_search(&dictionary, &conversion, 16)
    });
    let (actual, allocations) = crate::ime::personal_rerank::allocations::count(|| {
        search_converted(&dictionary, &conversion, 16)
    });
    let expected: Vec<_> = doubles[..16]
        .iter()
        .map(|text| JapaneseConversion {
            text: text.clone(),
            cost: 0,
        })
        .collect();
    assert_eq!(reference, expected);
    assert_eq!(actual, expected);
    eprintln!("日文密集双假名矩阵分配:{baseline_allocations}→{allocations}");
    assert!(baseline_allocations >= 200, "原笛卡尔积物化成本");
    assert!(
        allocations <= 38,
        "胜选移入输出,同分败选无需物化:{allocations}"
    );
}

#[test]
fn duplicate_row_texts_still_occupy_ranking_slots() {
    let mut row = Row::new();
    for index in 0..MAX_NODES_PER_ROW {
        row.extend("重复", "词面", 0, index as u16);
    }
    row.extend("后到", "其他词面", 0, 9);
    assert_eq!(row.nodes.len(), MAX_NODES_PER_ROW);
    assert!(row.nodes.iter().all(|node| node.text == "重复词面"));
    assert_eq!(
        row.nodes
            .iter()
            .map(|node| node.right_id)
            .collect::<Vec<_>>(),
        (0..8).collect::<Vec<_>>()
    );
}