Skip to main content

sudachi/dic/word_info/
layout.rs

1/*
2 * Copyright (c) 2026 Works Applications Co., Ltd.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17pub const ENTRY_INITIAL_OFFSET: usize = 32;
18pub const PARAMS_SIZE: usize = 6;
19pub const FIXED_PART_SIZE: usize = 32;
20pub const WORD_INFO_FIXED_SIZE: usize = FIXED_PART_SIZE - PARAMS_SIZE;
21pub const WORD_ID_ALIGNMENT_BITS: usize = 3;
22pub const WORD_INFO_OFFSET_ALIGNMENT: usize = 1 << WORD_ID_ALIGNMENT_BITS;
23
24pub const OFFSET_C_UNIT_SPLIT_LENGTH: usize = 26;
25pub const OFFSET_B_UNIT_SPLIT_LENGTH: usize = 27;
26pub const OFFSET_A_UNIT_SPLIT_LENGTH: usize = 28;
27pub const OFFSET_WORD_STRUCTURE_LENGTH: usize = 29;
28pub const OFFSET_SYNONYM_GROUP_IDS_LENGTH: usize = 30;
29pub const OFFSET_USER_DATA_FLAG: usize = 31;
30
31#[inline]
32pub const fn aligned_size(size: usize) -> usize {
33    (size + (WORD_INFO_OFFSET_ALIGNMENT - 1)) & !(WORD_INFO_OFFSET_ALIGNMENT - 1)
34}
35
36#[inline]
37pub fn embedded_len(len: i8) -> usize {
38    std::cmp::max(0, len) as usize
39}
40
41#[inline]
42pub fn is_valid_user_data_flag(flag: i8) -> bool {
43    matches!(flag, 0 | 1)
44}
45
46pub fn size_from_lengths(
47    c_len: i8,
48    b_len: i8,
49    a_len: i8,
50    ws_len: i8,
51    syn_len: i8,
52    user_data_units: Option<i16>,
53) -> Option<usize> {
54    let size = unaligned_size_from_lengths(c_len, b_len, a_len, ws_len, syn_len, user_data_units)?;
55    Some(aligned_size(size))
56}
57
58pub(crate) fn size_from_variable_layout(layout: WordInfoVariableLayout) -> Option<usize> {
59    size_from_lengths(
60        layout.c_unit_split_length,
61        layout.b_unit_split_length,
62        layout.a_unit_split_length,
63        layout.word_structure_length,
64        layout.synonym_group_ids_length,
65        layout.user_data_units(),
66    )
67}
68
69pub fn unaligned_size_from_lengths(
70    c_len: i8,
71    b_len: i8,
72    a_len: i8,
73    ws_len: i8,
74    syn_len: i8,
75    user_data_units: Option<i16>,
76) -> Option<usize> {
77    if c_len < 0 || syn_len < 0 {
78        return None;
79    }
80
81    let mut size = FIXED_PART_SIZE;
82    size = size.checked_add(4 * c_len as usize)?;
83    size = size.checked_add(4 * embedded_len(b_len))?;
84    size = size.checked_add(4 * embedded_len(a_len))?;
85    size = size.checked_add(4 * embedded_len(ws_len))?;
86    size = size.checked_add(4 * syn_len as usize)?;
87
88    if let Some(units) = user_data_units {
89        if units < 0 {
90            return None;
91        }
92        // put length as i16
93        size = size.checked_add(2 + units as usize * 2)?;
94    }
95
96    Some(size)
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub(crate) struct WordInfoVariableLayout {
101    pub c_unit_split_length: i8,
102    pub b_unit_split_length: i8,
103    pub a_unit_split_length: i8,
104    pub word_structure_length: i8,
105    pub synonym_group_ids_length: i8,
106    pub user_data_flag: i8,
107    pub user_data_units: i16,
108}
109
110impl WordInfoVariableLayout {
111    #[allow(clippy::too_many_arguments)]
112    pub fn new(
113        c_unit_split_len: usize,
114        b_unit_split_shared: bool,
115        b_unit_split_len: usize,
116        a_unit_split_shared: bool,
117        a_unit_split_len: usize,
118        word_structure_shared: bool,
119        word_structure_len: usize,
120        synonym_group_ids_len: usize,
121        user_data_units: usize,
122    ) -> Option<Self> {
123        let c_len = i8::try_from(c_unit_split_len).ok()?;
124        let b_len = if b_unit_split_shared {
125            -1
126        } else {
127            i8::try_from(b_unit_split_len).ok()?
128        };
129        let a_len = if a_unit_split_shared {
130            -1
131        } else {
132            i8::try_from(a_unit_split_len).ok()?
133        };
134        let ws_len = if word_structure_shared {
135            -1
136        } else {
137            i8::try_from(word_structure_len).ok()?
138        };
139        let syn_len = i8::try_from(synonym_group_ids_len).ok()?;
140        let user_data_units = i16::try_from(user_data_units).ok()?;
141        let user_data_flag = if user_data_units == 0 { 0 } else { 1 };
142        Some(Self {
143            c_unit_split_length: c_len,
144            b_unit_split_length: b_len,
145            a_unit_split_length: a_len,
146            word_structure_length: ws_len,
147            synonym_group_ids_length: syn_len,
148            user_data_flag,
149            user_data_units,
150        })
151    }
152
153    #[inline]
154    pub fn user_data_units(self) -> Option<i16> {
155        if self.user_data_flag == 0 {
156            None
157        } else {
158            Some(self.user_data_units)
159        }
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn computes_size_without_user_data() {
169        let size = size_from_lengths(2, -1, -1, 3, 1, None).unwrap();
170        assert_eq!(size, 56);
171    }
172
173    #[test]
174    fn computes_size_with_user_data() {
175        let size = size_from_lengths(1, 2, 3, -1, 2, Some(4)).unwrap();
176        assert_eq!(size, 80);
177    }
178
179    #[test]
180    fn computes_size_from_variable_layout() {
181        let layout = WordInfoVariableLayout::new(1, false, 2, false, 3, true, 3, 2, 4).unwrap();
182        let size = size_from_variable_layout(layout).unwrap();
183        assert_eq!(size, 80);
184    }
185
186    #[test]
187    fn rejects_invalid_lengths() {
188        assert!(size_from_lengths(-1, 0, 0, 0, 0, None).is_none());
189        assert!(size_from_lengths(0, 0, 0, 0, -1, None).is_none());
190        assert!(size_from_lengths(0, 0, 0, 0, 0, Some(-1)).is_none());
191    }
192
193    #[test]
194    fn variable_layout_marks_shared_split_arrays() {
195        let layout = WordInfoVariableLayout::new(2, true, 2, false, 1, true, 1, 3, 0).unwrap();
196        assert_eq!(layout.c_unit_split_length, 2);
197        assert_eq!(layout.b_unit_split_length, -1);
198        assert_eq!(layout.a_unit_split_length, 1);
199        assert_eq!(layout.word_structure_length, -1);
200        assert_eq!(layout.synonym_group_ids_length, 3);
201        assert_eq!(layout.user_data_flag, 0);
202        assert_eq!(layout.user_data_units, 0);
203    }
204
205    #[test]
206    fn variable_layout_marks_user_data_presence() {
207        let layout = WordInfoVariableLayout::new(0, false, 1, false, 2, false, 3, 4, 5).unwrap();
208        assert_eq!(layout.user_data_flag, 1);
209        assert_eq!(layout.user_data_units, 5);
210    }
211
212    #[test]
213    fn variable_layout_rejects_large_lengths() {
214        assert!(WordInfoVariableLayout::new(
215            usize::from(i8::MAX as u8) + 1,
216            false,
217            0,
218            false,
219            0,
220            false,
221            0,
222            0,
223            0,
224        )
225        .is_none());
226        assert!(WordInfoVariableLayout::new(
227            0,
228            false,
229            0,
230            false,
231            0,
232            false,
233            0,
234            0,
235            i16::MAX as usize + 1
236        )
237        .is_none());
238    }
239}