Skip to main content

vyre_primitives/decode/
hex.rs

1//! Hex decode primitive body.
2
3use std::sync::{Arc, OnceLock};
4
5use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
6use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
7
8/// Canonical op id for ASCII hex decode.
9pub const HEX_DECODE_OP_ID: &str = "vyre-primitives::decode::hex_decode";
10/// Number of words in the ASCII hex decode lookup table.
11pub const HEX_DECODE_TABLE_WORDS: u32 = 256;
12/// Canonical hex decode workgroup size.
13pub const HEX_WORKGROUP_SIZE: [u32; 3] = [64, 1, 1];
14
15static HEX_DECODE_TABLE: OnceLock<[u32; 256]> = OnceLock::new();
16
17/// Return the canonical 256-entry ASCII hex decode table by value.
18#[must_use]
19pub fn hex_decode_table() -> [u32; 256] {
20    *hex_decode_table_ref()
21}
22
23/// Process-wide canonical ASCII hex decode table.
24///
25/// The table is immutable after construction. Dispatch setup and CPU oracles
26/// should use this reference when they do not need an owned copy.
27#[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/// Number of decoded byte slots produced by an even-length hex input.
53#[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    // Canonical masked 256-table lookup (ONE-PLACE: crate::ir_safe).
60    crate::ir_safe::byte_table_lookup(table, byte)
61}
62
63/// Decode one hex byte pair into a single u32 byte value.
64#[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/// Build the reusable hex decode body.
73#[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/// Wrap the hex decode body as a child of `parent_op_id`.
96#[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/// Standalone hex decode program for primitive-level conformance.
114#[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/// CPU oracle for the primitive hex decode contract.
135///
136/// Invalid nibbles clamp to zero through the table, matching the GPU body.
137#[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}