logicaffeine_compile/model/tokenizer/
unicode.rs1use std::sync::OnceLock;
10
11use super::unicode_tables::{FLAG_RANGES, LOWERCASE_RUNS};
12
13pub const MAX_CODEPOINTS: u32 = 0x110000;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub struct Flags(pub u16);
19
20impl Flags {
21 pub const UNDEFINED: u16 = 0x0001;
22 pub const NUMBER: u16 = 0x0002;
23 pub const LETTER: u16 = 0x0004;
24 pub const SEPARATOR: u16 = 0x0008;
25 pub const ACCENT_MARK: u16 = 0x0010;
26 pub const PUNCTUATION: u16 = 0x0020;
27 pub const SYMBOL: u16 = 0x0040;
28 pub const CONTROL: u16 = 0x0080;
29 pub const MASK_CATEGORIES: u16 = 0x00FF;
30 pub const WHITESPACE: u16 = 0x0100;
31 pub const KEPT: u16 = 0x01FF;
33
34 pub fn is_undefined(self) -> bool {
35 self.0 & Self::UNDEFINED != 0
36 }
37 pub fn is_number(self) -> bool {
38 self.0 & Self::NUMBER != 0
39 }
40 pub fn is_letter(self) -> bool {
41 self.0 & Self::LETTER != 0
42 }
43 pub fn is_separator(self) -> bool {
44 self.0 & Self::SEPARATOR != 0
45 }
46 pub fn is_accent_mark(self) -> bool {
47 self.0 & Self::ACCENT_MARK != 0
48 }
49 pub fn is_punctuation(self) -> bool {
50 self.0 & Self::PUNCTUATION != 0
51 }
52 pub fn is_symbol(self) -> bool {
53 self.0 & Self::SYMBOL != 0
54 }
55 pub fn is_control(self) -> bool {
56 self.0 & Self::CONTROL != 0
57 }
58 pub fn is_whitespace(self) -> bool {
59 self.0 & Self::WHITESPACE != 0
60 }
61
62 pub fn category(self) -> u16 {
64 self.0 & Self::MASK_CATEGORIES
65 }
66}
67
68pub fn flags(cpt: u32) -> Flags {
71 if cpt >= MAX_CODEPOINTS {
72 return Flags(Flags::UNDEFINED);
73 }
74 let i = FLAG_RANGES.partition_point(|&e| e >> 9 <= cpt) - 1;
75 Flags((FLAG_RANGES[i] & 0x1FF) as u16)
76}
77
78pub fn flags_in(cpts: &[u32], lo: usize, hi: usize, pos: usize) -> Flags {
80 if lo <= pos && pos < hi {
81 flags(cpts[pos])
82 } else {
83 Flags(0)
84 }
85}
86
87pub fn to_lower(cpt: u32) -> u32 {
90 let i = LOWERCASE_RUNS.partition_point(|r| r.0 <= cpt);
91 if i == 0 {
92 return cpt;
93 }
94 let (first, count, stride, delta) = LOWERCASE_RUNS[i - 1];
95 let off = cpt - first;
96 if off % stride == 0 && off / stride < count {
97 (cpt as i64 + delta as i64) as u32
98 } else {
99 cpt
100 }
101}
102
103pub fn is_han(cpt: u32) -> bool {
105 matches!(cpt,
106 0x4E00..=0x9FFF
107 | 0x3400..=0x4DBF
108 | 0x20000..=0x2A6DF
109 | 0x2A700..=0x2B73F
110 | 0x2B740..=0x2B81F
111 | 0x2B820..=0x2CEAF
112 | 0x2CEB0..=0x2EBEF
113 | 0xF900..=0xFAFF
114 | 0x2F800..=0x2FA1F)
115}
116
117pub fn len_utf8(lead: u8) -> usize {
119 const LOOKUP: [usize; 16] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4];
120 LOOKUP[(lead >> 4) as usize]
121}
122
123fn cpt_from_utf8(b: &[u8], offset: &mut usize) -> Option<u32> {
126 let o = *offset;
127 let b0 = b[o] as u32;
128 let cont = |k: usize| b.get(o + k).is_some_and(|&c| c & 0xC0 == 0x80);
129 if b0 & 0x80 == 0 {
130 *offset += 1;
131 return Some(b0);
132 }
133 if b0 & 0x40 == 0 {
134 return None;
135 }
136 if b0 & 0x20 == 0 {
137 if !cont(1) {
138 return None;
139 }
140 *offset += 2;
141 return Some(((b0 & 0x1F) << 6) | (b[o + 1] as u32 & 0x3F));
142 }
143 if b0 & 0x10 == 0 {
144 if !(cont(1) && cont(2)) {
145 return None;
146 }
147 *offset += 3;
148 return Some(((b0 & 0x0F) << 12) | ((b[o + 1] as u32 & 0x3F) << 6) | (b[o + 2] as u32 & 0x3F));
149 }
150 if b0 & 0x08 == 0 {
151 if !(cont(1) && cont(2) && cont(3)) {
152 return None;
153 }
154 *offset += 4;
155 return Some(
156 ((b0 & 0x07) << 18) | ((b[o + 1] as u32 & 0x3F) << 12) | ((b[o + 2] as u32 & 0x3F) << 6) | (b[o + 3] as u32 & 0x3F),
157 );
158 }
159 None
160}
161
162pub fn cpts_from_utf8(text: &[u8]) -> Vec<u32> {
164 let mut out = Vec::with_capacity(text.len());
165 let mut offset = 0;
166 while offset < text.len() {
167 match cpt_from_utf8(text, &mut offset) {
168 Some(c) => out.push(c),
169 None => {
170 offset += 1;
171 out.push(0xFFFD);
172 }
173 }
174 }
175 out
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq)]
180pub struct InvalidCodepoint(pub u32);
181
182pub fn push_utf8(cpt: u32, out: &mut Vec<u8>) -> Result<(), InvalidCodepoint> {
184 match cpt {
185 0..=0x7F => out.push(cpt as u8),
186 0x80..=0x7FF => out.extend_from_slice(&[0xC0 | ((cpt >> 6) & 0x1F) as u8, 0x80 | (cpt & 0x3F) as u8]),
187 0x800..=0xFFFF => out.extend_from_slice(&[
188 0xE0 | ((cpt >> 12) & 0x0F) as u8,
189 0x80 | ((cpt >> 6) & 0x3F) as u8,
190 0x80 | (cpt & 0x3F) as u8,
191 ]),
192 0x10000..=0x10FFFF => out.extend_from_slice(&[
193 0xF0 | ((cpt >> 18) & 0x07) as u8,
194 0x80 | ((cpt >> 12) & 0x3F) as u8,
195 0x80 | ((cpt >> 6) & 0x3F) as u8,
196 0x80 | (cpt & 0x3F) as u8,
197 ]),
198 _ => return Err(InvalidCodepoint(cpt)),
199 }
200 Ok(())
201}
202
203fn byte_cpts() -> &'static [u32; 256] {
205 static MAP: OnceLock<[u32; 256]> = OnceLock::new();
206 MAP.get_or_init(|| {
207 let mut map = [u32::MAX; 256];
208 for ch in (0x21..=0x7E).chain(0xA1..=0xAC).chain(0xAE..=0xFF) {
209 map[ch as usize] = ch;
210 }
211 let mut n = 0;
212 for ch in 0..256 {
213 if map[ch] == u32::MAX {
214 map[ch] = 256 + n;
215 n += 1;
216 }
217 }
218 map
219 })
220}
221
222pub fn byte_to_utf8(b: u8, out: &mut Vec<u8>) {
224 push_utf8(byte_cpts()[b as usize], out).expect("the byte alphabet is below U+0200");
225}
226
227pub fn utf8_to_byte(cpt: u32) -> Option<u8> {
229 static INVERSE: OnceLock<std::collections::HashMap<u32, u8>> = OnceLock::new();
230 INVERSE
231 .get_or_init(|| byte_cpts().iter().enumerate().map(|(b, &c)| (c, b as u8)).collect())
232 .get(&cpt)
233 .copied()
234}