1use std::sync::{Arc, OnceLock};
4
5use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
6use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
7
8pub const HEX_DECODE_OP_ID: &str = "vyre-primitives::decode::hex_decode";
10pub const HEX_DECODE_TABLE_WORDS: u32 = 256;
12pub const HEX_WORKGROUP_SIZE: [u32; 3] = [64, 1, 1];
14
15static HEX_DECODE_TABLE: OnceLock<[u32; 256]> = OnceLock::new();
16
17#[must_use]
19pub fn hex_decode_table() -> [u32; 256] {
20 *hex_decode_table_ref()
21}
22
23#[must_use]
28pub fn hex_decode_table_ref() -> &'static [u32; 256] {
29 HEX_DECODE_TABLE.get_or_init(build_hex_decode_table)
30}
31
32fn build_hex_decode_table() -> [u32; 256] {
33 let mut table = [0u32; 256];
34 let mut byte = b'0';
35 while byte <= b'9' {
36 table[byte as usize] = u32::from(byte - b'0');
37 byte += 1;
38 }
39 byte = b'A';
40 while byte <= b'F' {
41 table[byte as usize] = u32::from(byte - b'A' + 10);
42 byte += 1;
43 }
44 byte = b'a';
45 while byte <= b'f' {
46 table[byte as usize] = u32::from(byte - b'a' + 10);
47 byte += 1;
48 }
49 table
50}
51
52#[must_use]
54pub const fn hex_decoded_capacity(input_len: u32) -> u32 {
55 input_len / 2
56}
57
58fn nibble_expr(byte: Expr, table: &str) -> Expr {
59 crate::ir_safe::byte_table_lookup(table, byte)
61}
62
63#[must_use]
65pub fn hex_decode_pair_expr(input: &str, table: &str, pair: Expr) -> Expr {
66 let in_base = Expr::mul(pair, Expr::u32(2));
67 let hi = nibble_expr(Expr::load(input, in_base.clone()), table);
68 let lo = nibble_expr(Expr::load(input, Expr::add(in_base, Expr::u32(1))), table);
69 Expr::bitor(Expr::shl(hi, Expr::u32(4)), lo)
70}
71
72#[must_use]
74pub fn hex_decode_body(input: &str, output: &str, table: &str, input_len: u32) -> Vec<Node> {
75 if input_len % 2 != 0 {
76 return vec![Node::trap(
77 Expr::u32(input_len),
78 "Fix: hex_decode requires an even input_len; reject the dangling nibble upstream",
79 )];
80 }
81 let output_len = hex_decoded_capacity(input_len);
82 vec![
83 Node::let_bind("pair", Expr::InvocationId { axis: 0 }),
84 Node::if_then(
85 Expr::lt(Expr::var("pair"), Expr::u32(output_len)),
86 vec![Node::store(
87 output,
88 Expr::var("pair"),
89 hex_decode_pair_expr(input, table, Expr::var("pair")),
90 )],
91 ),
92 ]
93}
94
95#[must_use]
97pub fn hex_decode_child(
98 parent_op_id: &str,
99 input: &str,
100 output: &str,
101 table: &str,
102 input_len: u32,
103) -> Node {
104 Node::Region {
105 generator: Ident::from(HEX_DECODE_OP_ID),
106 source_region: Some(GeneratorRef {
107 name: parent_op_id.to_string(),
108 }),
109 body: Arc::new(hex_decode_body(input, output, table, input_len)),
110 }
111}
112
113#[must_use]
115pub fn hex_decode(input: &str, output: &str, table: &str, input_len: u32) -> Program {
116 Program::wrapped(
117 vec![
118 BufferDecl::storage(input, 0, BufferAccess::ReadOnly, DataType::U32)
119 .with_count(input_len),
120 BufferDecl::output(output, 1, DataType::U32)
121 .with_count(hex_decoded_capacity(input_len)),
122 BufferDecl::storage(table, 2, BufferAccess::ReadOnly, DataType::U32)
123 .with_count(HEX_DECODE_TABLE_WORDS),
124 ],
125 HEX_WORKGROUP_SIZE,
126 vec![Node::Region {
127 generator: Ident::from(HEX_DECODE_OP_ID),
128 source_region: None,
129 body: Arc::new(hex_decode_body(input, output, table, input_len)),
130 }],
131 )
132}
133
134#[must_use]
138#[cfg(any(test, feature = "cpu-parity"))]
139pub fn hex_decode_reference_packed(input: &[u8]) -> Vec<u32> {
140 assert!(input.len() % 2 == 0, "hex input must contain byte pairs");
141 let table = hex_decode_table_ref();
142 input
143 .chunks_exact(2)
144 .map(|pair| {
145 let hi = table[usize::from(pair[0])];
146 let lo = table[usize::from(pair[1])];
147 (hi << 4) | lo
148 })
149 .collect()
150}
151
152#[cfg(feature = "inventory-registry")]
153inventory::submit! {
154 crate::harness::OpEntry::new(
155 HEX_DECODE_OP_ID,
156 || hex_decode("input", "output", "table", 6),
157 Some(|| vec![vec![
158 crate::wire::pack_u32_slice(&[
159 u32::from(b'4'),
160 u32::from(b'D'),
161 u32::from(b'6'),
162 u32::from(b'1'),
163 u32::from(b'6'),
164 u32::from(b'E'),
165 ]),
166 vec![0; 12],
167 crate::wire::pack_u32_slice(hex_decode_table_ref()),
168 ]]),
169 Some(|| vec![vec![crate::wire::pack_u32_slice(&[0x4D, 0x61, 0x6E])]]),
170 )
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn reference_decodes_upper_lower_and_invalid_nibbles() {
179 assert_eq!(
180 hex_decode_reference_packed(b"4D6aZ1"),
181 vec![0x4D, 0x6A, 0x01]
182 );
183 }
184
185 #[test]
186 fn hex_decode_table_ref_matches_value_api_and_reuses_allocation() {
187 let first = hex_decode_table_ref();
188 let second = hex_decode_table_ref();
189 assert!(
190 std::ptr::eq(first, second),
191 "Fix: hex decode setup must reuse the immutable primitive table instead of rebuilding it per dispatch."
192 );
193 assert_eq!(*first, hex_decode_table());
194 }
195
196 #[test]
197 fn odd_length_lowers_to_trap_not_silent_truncation() {
198 let body = hex_decode_body("input", "output", "table", 3);
199 assert!(matches!(body.as_slice(), [Node::Trap { .. }]));
200 }
201
202 #[test]
203 fn standalone_program_is_single_primitive_region() {
204 let program = hex_decode("input", "output", "table", 6);
205 let [Node::Region { generator, .. }] = program.entry() else {
206 panic!("expected one primitive hex decode region");
207 };
208 assert_eq!(generator.as_str(), HEX_DECODE_OP_ID);
209 }
210}