Skip to main content

polydat_nodes/
bytebuf.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Byte buffer and character buffer nodes.
5//!
6//! Two composition patterns from nosqlbench:
7//!
8//! 1. **Direct hash fill**: generate N bytes from a seed by chaining
9//!    hashes. Fresh per cycle. Simple but slower for large buffers.
10//!
11//! 2. **Image extraction**: pre-fill a large static buffer at init
12//!    time, then extract variable-length slices at cycle time using
13//!    hash-based offset selection. Fast hot path — just a memcpy.
14
15#[cfg(test)]
16use polydat::ast::{PolydatNode, Value};
17
18// =================================================================
19// Direct byte generation
20// =================================================================
21
22/// Convert a u64 to 8 bytes (little-endian).
23#[polydat::polydat_node(category = ByteBuffers)]
24fn u64_to_bytes(input: u64) -> Vec<u8> {
25    input.to_le_bytes().to_vec()
26}
27
28/// Generate N deterministic bytes from a u64 seed via chained hashing.
29/// Each 8-byte chunk is `hash(seed + chunk_index)`. Buffer is fresh
30/// per cycle.
31#[polydat::polydat_node(category = ByteBuffers)]
32fn bytes_from_hash(
33    input: u64,
34    #[poly_default(8u64)] size: polydat::derive_support::Const<u64>,
35) -> Vec<u8> {
36    let sz = *size as usize;
37    let mut result = Vec::with_capacity(sz);
38    let chunks = sz.div_ceil(8);
39    for i in 0..chunks {
40        let h = crate::hash::splitmix64_u64(input.wrapping_add(i as u64));
41        let take = (sz - result.len()).min(8);
42        result.extend_from_slice(&h.to_le_bytes()[..take]);
43    }
44    result
45}
46
47// =================================================================
48// Image-based extraction (init-time buffer, cycle-time slice)
49// =================================================================
50
51/// A pre-filled byte image for fast cycle-time extraction.
52///
53/// Built at init time by hash-filling a large buffer. At cycle time,
54/// a hash-based offset selects where to extract a variable-length
55/// slice. The extraction is just a memcpy — no per-byte computation.
56pub struct ByteImage {
57    image: Vec<u8>,
58}
59
60impl ByteImage {
61    /// Build a byte image of `image_size` bytes from a seed.
62    pub fn new(image_size: usize, seed: u64) -> Self {
63        let mut image = Vec::with_capacity(image_size);
64        let chunks = image_size.div_ceil(8);
65        for i in 0..chunks {
66            let h = crate::hash::splitmix64_u64(seed.wrapping_add(i as u64));
67            let take = (image_size - image.len()).min(8);
68            image.extend_from_slice(&h.to_le_bytes()[..take]);
69        }
70        Self { image }
71    }
72
73    /// Extract a slice at the given hash-based offset.
74    pub fn extract(&self, hash_val: u64, slice_size: usize) -> &[u8] {
75        let max_offset = self.image.len().saturating_sub(slice_size);
76        let offset = if max_offset > 0 {
77            (hash_val as usize) % (max_offset + 1)
78        } else {
79            0
80        };
81        let end = (offset + slice_size).min(self.image.len());
82        &self.image[offset..end]
83    }
84}
85
86/// Setup function: build the byte image from `image_size` + `seed`.
87/// Single-call construction-time invocation per node instance.
88fn build_byte_image(image_size: u64, seed: u64) -> ByteImage {
89    ByteImage::new(image_size as usize, seed)
90}
91
92/// Extract a fixed-size byte slice from a pre-built image.
93///
94/// Signature: `byte_image_extract(input: u64) -> (output: bytes)`
95/// Const: `image_size: u64`, `slice_size: u64`, `seed: u64`
96///
97/// The image is built at init time from `image_size` + `seed`. Each
98/// cycle, the input u64 selects the extraction offset via modular
99/// arithmetic and a `slice_size`-long span is copied out.
100///
101/// The image is a multi-source `#[poly_const(... from = (image_size,
102/// seed))]`; `slice_size` is a per-node `Const<u64>` consumed in the
103/// body.
104#[polydat::polydat_node(category = ByteBuffers)]
105fn byte_image_extract(
106    input: u64,
107    image_size: polydat::derive_support::Const<u64>,
108    slice_size: polydat::derive_support::Const<u64>,
109    seed: polydat::derive_support::Const<u64>,
110    #[poly_const(build_byte_image, from = (image_size, seed))] image: &ByteImage,
111) -> Vec<u8> {
112    let _ = image_size; // captured in `image`; field kept for workload-author surface
113    let _ = seed;
114    image.extract(input, *slice_size as usize).to_vec()
115}
116
117/// A pre-filled character image for fast text extraction.
118///
119/// Built at init time by cycling through a character set to fill a
120/// buffer. At cycle time, a hash-based offset extracts a substring.
121/// This is the Rust equivalent of nosqlbench's `CharBufImage`.
122pub struct CharImage {
123    image: String,
124}
125
126impl CharImage {
127    /// Build a character image by repeating `charset` to fill `size` chars.
128    pub fn new(charset: &str, size: usize) -> Self {
129        let chars: Vec<char> = parse_charset(charset);
130        assert!(!chars.is_empty(), "charset must not be empty");
131        let mut image = String::with_capacity(size);
132        for idx in 0..size {
133            image.push(chars[idx % chars.len()]);
134        }
135        Self { image }
136    }
137
138    /// Build a character image by hashing into the charset.
139    pub fn hashed(charset: &str, size: usize, seed: u64) -> Self {
140        let chars: Vec<char> = parse_charset(charset);
141        assert!(!chars.is_empty(), "charset must not be empty");
142        let mut image = String::with_capacity(size);
143        for i in 0..size {
144            let h = crate::hash::splitmix64_u64(seed.wrapping_add(i as u64));
145            image.push(chars[(h as usize) % chars.len()]);
146        }
147        Self { image }
148    }
149
150    fn extract(&self, hash_val: u64, slice_len: usize) -> &str {
151        let chars: Vec<(usize, char)> = self.image.char_indices().collect();
152        let max_start = chars.len().saturating_sub(slice_len);
153        let start_idx = if max_start > 0 {
154            (hash_val as usize) % (max_start + 1)
155        } else {
156            0
157        };
158        let end_idx = (start_idx + slice_len).min(chars.len());
159        let byte_start = chars[start_idx].0;
160        let byte_end = if end_idx < chars.len() {
161            chars[end_idx].0
162        } else {
163            self.image.len()
164        };
165        &self.image[byte_start..byte_end]
166    }
167}
168
169/// Setup function: build the character image from `charset` +
170/// `image_size` + `seed`. Single-call construction-time invocation.
171fn build_char_image(charset: &str, image_size: u64, seed: u64) -> CharImage {
172    CharImage::hashed(charset, image_size as usize, seed)
173}
174
175/// Extract a text slice from a pre-built character image.
176///
177/// Signature: `char_image_extract(input: u64) -> (output: Str)`
178/// Const: `charset: Str`, `image_size: u64`, `slice_size: u64`,
179///        `seed: u64` (default 0)
180///
181/// Equivalent to nosqlbench's `CharBufImage`. The image is filled
182/// from the charset at init time. Each cycle extracts a substring.
183#[polydat::polydat_node(category = ByteBuffers)]
184fn char_image_extract(
185    input: u64,
186    charset: polydat::derive_support::Const<&str>,
187    image_size: polydat::derive_support::Const<u64>,
188    slice_size: polydat::derive_support::Const<u64>,
189    #[poly_default(0u64)] seed: polydat::derive_support::Const<u64>,
190    #[poly_const(build_char_image, from = (charset, image_size, seed))] image: &CharImage,
191) -> String {
192    let _ = charset;
193    let _ = image_size;
194    let _ = seed;
195    image.extract(input, *slice_size as usize).to_string()
196}
197
198// =================================================================
199// Byte slice and hex conversion
200// =================================================================
201
202/// Extract a sub-range from a byte buffer.
203#[polydat::polydat_node(category = ByteBuffers)]
204fn byte_slice(
205    input: &[u8],
206    #[poly_default(0u64)] offset: polydat::derive_support::Const<u64>,
207    #[poly_default(8u64)] length: polydat::derive_support::Const<u64>,
208) -> Vec<u8> {
209    let off = *offset as usize;
210    let len = *length as usize;
211    let end = (off + len).min(input.len());
212    let start = off.min(end);
213    input[start..end].to_vec()
214}
215
216const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
217
218/// Encode bytes as lowercase hexadecimal string.
219#[polydat::polydat_node(category = ByteBuffers)]
220fn to_hex(input: &[u8]) -> String {
221    let mut out = String::with_capacity(input.len() * 2);
222    for &b in input {
223        out.push(HEX_CHARS[(b >> 4) as usize] as char);
224        out.push(HEX_CHARS[(b & 0x0f) as usize] as char);
225    }
226    out
227}
228
229#[inline(always)]
230fn hex_val(c: u8) -> Option<u8> {
231    match c {
232        b'0'..=b'9' => Some(c - b'0'),
233        b'a'..=b'f' => Some(c - b'a' + 10),
234        b'A'..=b'F' => Some(c - b'A' + 10),
235        _ => None,
236    }
237}
238
239/// Decode a hexadecimal string to bytes.
240#[polydat::polydat_node(category = ByteBuffers)]
241fn from_hex(input: &str) -> Vec<u8> {
242    let bytes = input.as_bytes();
243    let mut out = Vec::with_capacity(bytes.len() / 2);
244    let mut i = 0;
245    while i + 1 < bytes.len() {
246        if let (Some(h), Some(l)) = (hex_val(bytes[i]), hex_val(bytes[i + 1])) {
247            out.push((h << 4) | l);
248        }
249        i += 2;
250    }
251    out
252}
253
254// --- charset parser (shared with string::Combinations) ---
255
256fn parse_charset(spec: &str) -> Vec<char> {
257    let mut chars = Vec::new();
258    let spec_chars: Vec<char> = spec.chars().collect();
259    let mut i = 0;
260    while i < spec_chars.len() {
261        if i + 2 < spec_chars.len() && spec_chars[i + 1] == '-' {
262            for c in spec_chars[i]..=spec_chars[i + 2] {
263                chars.push(c);
264            }
265            i += 3;
266        } else {
267            chars.push(spec_chars[i]);
268            i += 1;
269        }
270    }
271    chars
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn u64_to_bytes_roundtrip() {
280        let node = U64ToBytes::new();
281        let mut out = [Value::None];
282        node.eval(&[Value::U64(0xDEADBEEF)], &mut out);
283        let bytes = out[0].as_bytes();
284        assert_eq!(bytes.len(), 8);
285        assert_eq!(u64::from_le_bytes(bytes.try_into().unwrap()), 0xDEADBEEF);
286    }
287
288    #[test]
289    fn bytes_from_hash_size() {
290        let node = BytesFromHash::new(32);
291        let mut out = [Value::None];
292        node.eval(&[Value::U64(42)], &mut out);
293        assert_eq!(out[0].as_bytes().len(), 32);
294    }
295
296    #[test]
297    fn bytes_from_hash_deterministic() {
298        let node = BytesFromHash::new(16);
299        let mut out1 = [Value::None];
300        let mut out2 = [Value::None];
301        node.eval(&[Value::U64(42)], &mut out1);
302        node.eval(&[Value::U64(42)], &mut out2);
303        assert_eq!(out1[0].as_bytes(), out2[0].as_bytes());
304    }
305
306    #[test]
307    fn byte_image_extract_consistent_size() {
308        let node = ByteImageExtract::new(10000, 100, 0);
309        let mut out = [Value::None];
310        for i in 0..100u64 {
311            node.eval(&[Value::U64(i)], &mut out);
312            assert_eq!(out[0].as_bytes().len(), 100);
313        }
314    }
315
316    #[test]
317    fn byte_image_extract_deterministic() {
318        let node = ByteImageExtract::new(10000, 50, 0);
319        let mut out1 = [Value::None];
320        let mut out2 = [Value::None];
321        node.eval(&[Value::U64(42)], &mut out1);
322        node.eval(&[Value::U64(42)], &mut out2);
323        assert_eq!(out1[0].as_bytes(), out2[0].as_bytes());
324    }
325
326    #[test]
327    fn char_image_extract_size() {
328        let node = CharImageExtract::new("A-Za-z0-9".to_string(), 10000, 50, 0);
329        let mut out = [Value::None];
330        node.eval(&[Value::U64(42)], &mut out);
331        assert_eq!(out[0].as_str().len(), 50);
332    }
333
334    #[test]
335    fn char_image_extract_charset() {
336        let node = CharImageExtract::new("A-Z".to_string(), 1000, 20, 0);
337        let mut out = [Value::None];
338        node.eval(&[Value::U64(42)], &mut out);
339        assert!(out[0].as_str().chars().all(|c| c.is_ascii_uppercase()));
340    }
341
342    #[test]
343    fn char_image_extract_varied() {
344        let node = CharImageExtract::new("A-Za-z0-9".to_string(), 10000, 30, 0);
345        let mut out1 = [Value::None];
346        let mut out2 = [Value::None];
347        node.eval(&[Value::U64(0)], &mut out1);
348        node.eval(&[Value::U64(999)], &mut out2);
349        assert_ne!(out1[0].as_str(), out2[0].as_str());
350    }
351
352    #[test]
353    fn byte_slice_basic() {
354        let node = ByteSlice::new(2, 3);
355        let mut out = [Value::None];
356        node.eval(
357            &[Value::Bytes(vec![10u8, 20, 30, 40, 50].into())],
358            &mut out[..],
359        );
360        assert_eq!(out[0].as_bytes(), &[30, 40, 50]);
361    }
362
363    #[test]
364    fn hex_roundtrip() {
365        let to = ToHex::new();
366        let from = FromHex::new();
367        let mut mid = [Value::None];
368        let mut out = [Value::None];
369        let input = vec![0xDE, 0xAD, 0xBE, 0xEF];
370        to.eval(&[Value::Bytes(input.clone().into())], &mut mid[..]);
371        assert_eq!(mid[0].as_str(), "deadbeef");
372        from.eval(&[mid[0].clone()], &mut out);
373        assert_eq!(out[0].as_bytes(), &input);
374    }
375}