1use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
14
15pub const C_EOF: u32 = 0;
17pub const C_WS: u32 = 1;
19pub const C_NEWLINE: u32 = 2;
21pub const C_ALPHA: u32 = 3;
23pub const C_DIGIT: u32 = 4;
25pub const C_OPEN_PAREN: u32 = 5;
27pub const C_CLOSE_PAREN: u32 = 6;
29pub const C_OPEN_BRACE: u32 = 7;
31pub const C_CLOSE_BRACE: u32 = 8;
33pub const C_SEMICOLON: u32 = 9;
35pub const C_COMMA: u32 = 10;
37pub const C_DOT: u32 = 11;
39pub const C_STAR: u32 = 12;
41pub const C_PLUS: u32 = 13;
43pub const C_MINUS: u32 = 14;
45pub const C_SLASH: u32 = 15;
47pub const C_HASH: u32 = 16;
49pub const C_QUOTE: u32 = 17;
51pub const C_DQUOTE: u32 = 18;
53pub const C_EQUALS: u32 = 19;
55pub const C_LT: u32 = 20;
57pub const C_GT: u32 = 21;
59pub const C_BANG: u32 = 22;
61pub const C_AMP: u32 = 23;
63pub const C_PIPE: u32 = 24;
65pub const C_CARET: u32 = 25;
67pub const C_TILDE: u32 = 26;
69pub const C_PERCENT: u32 = 27;
71pub const C_BACKSLASH: u32 = 28;
73pub const C_OPEN_BRACKET: u32 = 29;
75pub const C_CLOSE_BRACKET: u32 = 30;
77pub const C_OTHER: u32 = 31;
79
80pub const CHAR_CLASS_OP_ID: &str = "vyre-primitives::text::char_class";
82pub const CHAR_CLASS_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
84
85#[must_use]
87pub const fn char_class_dispatch_grid(n: u32) -> [u32; 3] {
88 let blocks = n.div_ceil(CHAR_CLASS_WORKGROUP_SIZE[0]);
89 if blocks == 0 {
90 [1, 1, 1]
91 } else {
92 [blocks, 1, 1]
93 }
94}
95
96#[must_use]
98pub fn build_char_class_table() -> [u32; 256] {
99 let mut table = [C_OTHER; 256];
100
101 table[0] = C_EOF;
102 table[usize::from(b' ')] = C_WS;
103 table[usize::from(b'\t')] = C_WS;
104 table[usize::from(b'\n')] = C_NEWLINE;
105 table[usize::from(b'\r')] = C_NEWLINE;
106 table[usize::from(b'(')] = C_OPEN_PAREN;
107 table[usize::from(b')')] = C_CLOSE_PAREN;
108 table[usize::from(b'{')] = C_OPEN_BRACE;
109 table[usize::from(b'}')] = C_CLOSE_BRACE;
110 table[usize::from(b';')] = C_SEMICOLON;
111 table[usize::from(b',')] = C_COMMA;
112 table[usize::from(b'.')] = C_DOT;
113 table[usize::from(b'*')] = C_STAR;
114 table[usize::from(b'+')] = C_PLUS;
115 table[usize::from(b'-')] = C_MINUS;
116 table[usize::from(b'/')] = C_SLASH;
117 table[usize::from(b'#')] = C_HASH;
118 table[usize::from(b'\'')] = C_QUOTE;
119 table[usize::from(b'"')] = C_DQUOTE;
120 table[usize::from(b'=')] = C_EQUALS;
121 table[usize::from(b'<')] = C_LT;
122 table[usize::from(b'>')] = C_GT;
123 table[usize::from(b'!')] = C_BANG;
124 table[usize::from(b'&')] = C_AMP;
125 table[usize::from(b'|')] = C_PIPE;
126 table[usize::from(b'^')] = C_CARET;
127 table[usize::from(b'~')] = C_TILDE;
128 table[usize::from(b'%')] = C_PERCENT;
129 table[usize::from(b'\\')] = C_BACKSLASH;
130 table[usize::from(b'[')] = C_OPEN_BRACKET;
131 table[usize::from(b']')] = C_CLOSE_BRACKET;
132 table[usize::from(b'_')] = C_ALPHA;
133
134 for byte in b'0'..=b'9' {
135 table[usize::from(byte)] = C_DIGIT;
136 }
137 for byte in b'A'..=b'Z' {
138 table[usize::from(byte)] = C_ALPHA;
139 }
140 for byte in b'a'..=b'z' {
141 table[usize::from(byte)] = C_ALPHA;
142 }
143
144 table
145}
146
147fn char_class_body(source: &str, classified: &str, n: u32) -> Vec<Node> {
148 vec![Node::Region {
149 generator: vyre_foundation::ir::model::expr::Ident::from(CHAR_CLASS_OP_ID),
150 source_region: None,
151 body: std::sync::Arc::new(vec![
152 Node::let_bind("idx", Expr::InvocationId { axis: 0 }),
153 Node::if_then(
154 Expr::lt(Expr::var("idx"), Expr::u32(n)),
155 vec![Node::store(
156 classified,
157 Expr::var("idx"),
158 crate::ir_safe::source_byte_table_lookup("table", source, Expr::var("idx")),
162 )],
163 ),
164 ]),
165 }]
166}
167
168#[must_use]
175pub fn char_class(source: &str, classified: &str, n: u32) -> Program {
176 char_class_with_source_type(source, classified, n, DataType::U32)
177}
178
179#[must_use]
184pub fn char_class_u8(source: &str, classified: &str, n: u32) -> Program {
185 char_class_with_source_type(source, classified, n, DataType::U8)
186}
187
188fn char_class_with_source_type(
189 source: &str,
190 classified: &str,
191 n: u32,
192 source_type: DataType,
193) -> Program {
194 let output_byte_len = usize::try_from(n).unwrap_or(usize::MAX).saturating_mul(4);
195 Program::wrapped(
196 vec![
197 BufferDecl::storage(source, 0, BufferAccess::ReadOnly, source_type).with_count(n),
198 BufferDecl::storage("table", 1, BufferAccess::ReadOnly, DataType::U32).with_count(256),
199 BufferDecl::output(classified, 2, DataType::U32)
200 .with_count(n.max(1))
201 .with_output_byte_range(0..output_byte_len),
202 ],
203 CHAR_CLASS_WORKGROUP_SIZE,
204 char_class_body(source, classified, n),
205 )
206}
207
208#[must_use]
212#[cfg(any(test, feature = "cpu-parity", feature = "text"))]
213pub fn reference_char_class(source: &[u8], table: &[u32; 256]) -> Vec<u32> {
214 source
215 .iter()
216 .map(|byte| table[usize::from(*byte)])
217 .collect()
218}
219
220#[must_use]
222pub fn pack_u32(words: &[u32]) -> Vec<u8> {
223 crate::wire::pack_u32_slice(words)
224}
225
226#[must_use]
229pub fn pack_bytes_as_u32(bytes: &[u8]) -> Vec<u8> {
230 crate::wire::pack_bytes_as_u32_slice(bytes)
231}
232
233#[cfg(feature = "inventory-registry")]
234inventory::submit! {
235 vyre_foundation::operation::OperationRegistration::primitive(
236 CHAR_CLASS_OP_ID,
237 || char_class("source", "classified", 3),
238 Some(|| {
239 let table = build_char_class_table();
240 vec![vec![
241 pack_bytes_as_u32(b"A1 "),
242 pack_u32(&table),
243 vec![0u8; 3 * 4],
244 ]]
245 }),
246 Some(|| {
247 vec![vec![pack_u32(&[C_ALPHA, C_DIGIT, C_WS])]]
248 }),
249 )
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn table_classifies_ascii_letter_as_alpha() {
258 let table = build_char_class_table();
259 assert_eq!(table[usize::from(b'A')], C_ALPHA);
260 assert_eq!(table[usize::from(b'z')], C_ALPHA);
261 assert_eq!(table[usize::from(b'_')], C_ALPHA);
262 }
263
264 #[test]
265 fn table_classifies_digits() {
266 let table = build_char_class_table();
267 for byte in b'0'..=b'9' {
268 assert_eq!(table[usize::from(byte)], C_DIGIT);
269 }
270 }
271
272 #[test]
273 fn reference_walks_table() {
274 let table = build_char_class_table();
275 assert_eq!(
276 reference_char_class(b"A1 ", &table),
277 vec![C_ALPHA, C_DIGIT, C_WS]
278 );
279 }
280
281 #[test]
282 fn reference_covers_every_byte_value() {
283 let table = build_char_class_table();
284 let source: Vec<u8> = (0u8..=255).collect();
285 assert_eq!(reference_char_class(&source, &table), table.to_vec());
286 }
287
288 #[test]
289 fn program_uses_block_sized_workgroup() {
290 let program = char_class("source", "classified", 513);
291 assert_eq!(program.workgroup_size(), CHAR_CLASS_WORKGROUP_SIZE);
292 }
293
294 #[test]
295 fn packed_u8_program_declares_one_source_byte_per_element() {
296 let program = char_class_u8("source", "classified", 513);
297 let source = program
298 .buffers()
299 .iter()
300 .find(|buffer| buffer.name() == "source")
301 .expect("Fix: packed-u8 char_class source buffer must be declared");
302 let classified = program
303 .buffers()
304 .iter()
305 .find(|buffer| buffer.name() == "classified")
306 .expect("Fix: char_class output buffer must be declared");
307
308 assert_eq!(source.element(), DataType::U8);
309 assert_eq!(source.count(), 513);
310 assert_eq!(classified.element(), DataType::U32);
311 assert_eq!(classified.count(), 513);
312 assert_eq!(classified.output_byte_range(), Some(0..513 * 4));
313 assert_eq!(program.workgroup_size(), CHAR_CLASS_WORKGROUP_SIZE);
314 }
315
316 #[test]
317 fn empty_program_declares_empty_output_range() {
318 let program = char_class_u8("source", "classified", 0);
319 let classified = program
320 .buffers()
321 .iter()
322 .find(|buffer| buffer.name() == "classified")
323 .expect("Fix: char_class output buffer must be declared");
324
325 assert_eq!(classified.count(), 1);
326 assert_eq!(classified.output_byte_range(), Some(0..0));
327 }
328
329 #[test]
330 fn dispatch_grid_packs_byte_lanes_into_blocks() {
331 assert_eq!(char_class_dispatch_grid(0), [1, 1, 1]);
332 assert_eq!(char_class_dispatch_grid(1), [1, 1, 1]);
333 assert_eq!(char_class_dispatch_grid(256), [1, 1, 1]);
334 assert_eq!(char_class_dispatch_grid(257), [2, 1, 1]);
335 assert_eq!(char_class_dispatch_grid(513), [3, 1, 1]);
336 }
337
338 #[test]
339 fn primitive_id_names_the_primitive_tier() {
340 assert_eq!(CHAR_CLASS_OP_ID, "vyre-primitives::text::char_class");
341 }
342}