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