1use std::cell::RefCell;
16
17use xxhash_rust::xxh3::xxh3_64;
18
19pub static LOREM_IPSUM: &str = include_str!("../data/lorem_ipsum_full.txt");
25pub static NAMES: &str = include_str!("../data/names.txt");
27pub static LASTNAMES: &str = include_str!("../data/lastnames.txt");
29pub static CAREERS: &str = include_str!("../data/careers.txt");
31pub static COMPANIES: &str = include_str!("../data/companies.txt");
33pub static VARIABLE_WORDS: &str = include_str!("../data/variable_words.txt");
35
36thread_local! {
41 static RNG: RefCell<u64> = RefCell::new(
42 std::time::SystemTime::now()
43 .duration_since(std::time::UNIX_EPOCH)
44 .unwrap()
45 .as_nanos() as u64
46 );
47}
48
49fn next_u64() -> u64 {
50 RNG.with(|r| {
51 let mut s = *r.borrow();
52 s ^= s << 13;
53 s ^= s >> 7;
54 s ^= s << 17;
55 *r.borrow_mut() = s;
56 s
57 })
58}
59
60fn next_f64() -> f64 {
61 next_u64() as f64 / u64::MAX as f64
62}
63
64#[polydat::polydat_node(
71 category = Probability,
72 purity = Nondeterministic("thread-local PRNG"),
73)]
74fn random_range(
75 #[poly_default(0u64)] min: polydat::derive_support::Const<u64>,
76 #[poly_default(100u64)] max: polydat::derive_support::Const<u64>,
77) -> u64 {
78 let range = max.saturating_sub(*min).max(1);
82 *min + (next_u64() % range)
83}
84
85#[polydat::polydat_node(
87 category = Probability,
88 purity = Nondeterministic("thread-local PRNG"),
89)]
90fn random_f64(
91 #[poly_default(0.0f64)] min: polydat::derive_support::Const<f64>,
92 #[poly_default(1.0f64)] max: polydat::derive_support::Const<f64>,
93) -> f64 {
94 *min + next_f64() * (*max - *min)
95}
96
97#[polydat::polydat_node(
99 category = Probability,
100 purity = Nondeterministic("thread-local PRNG"),
101)]
102fn random_bytes(#[poly_default(8u64)] size: polydat::derive_support::Const<u64>) -> Vec<u8> {
103 let sz = *size as usize;
104 let mut buf = Vec::with_capacity(sz);
105 while buf.len() < sz {
106 let take = (sz - buf.len()).min(8);
107 buf.extend_from_slice(&next_u64().to_le_bytes()[..take]);
108 }
109 buf
110}
111
112pub struct Charset(pub Vec<char>);
115
116impl polydat::derive_support::PolydatSetup for Charset {}
117
118impl Charset {
119 pub fn parse(spec: &str) -> Self {
122 Self(parse_charset(spec))
123 }
124}
125
126#[polydat::polydat_node(
132 category = Probability,
133 purity = Nondeterministic("thread-local PRNG"),
134)]
135fn random_string(
136 #[poly_default("A-Za-z0-9")] charset: polydat::derive_support::Const<&str>,
137 #[poly_default(8u64)] length: polydat::derive_support::Const<u64>,
138 #[poly_const(Charset::parse, from = charset)] chars: &Charset,
139) -> String {
140 let _ = charset; let chars = &chars.0;
142 if chars.is_empty() {
143 return String::new();
144 }
145 (0..*length)
146 .map(|_| chars[(next_u64() as usize) % chars.len()])
147 .collect()
148}
149
150#[polydat::polydat_node(
152 category = Probability,
153 purity = Nondeterministic("thread-local PRNG"),
154)]
155fn random_bool(#[poly_default(0.5f64)] probability: polydat::derive_support::Const<f64>) -> bool {
156 let threshold = (probability.clamp(0.0, 1.0) * u64::MAX as f64) as u64;
157 next_u64() < threshold
158}
159
160impl RandomString {
161 pub fn alphanumeric(length: u64) -> Self {
163 Self::new("A-Za-z0-9".to_string(), length)
164 }
165}
166
167#[polydat::polydat_node(category = String)]
178fn hashed_lorem_extract(
179 input: u64,
180 min_len: polydat::derive_support::Const<u64>,
181 max_len: polydat::derive_support::Const<u64>,
182) -> String {
183 let min_len = *min_len as usize;
184 let max_len = *max_len as usize;
185 let len_range = max_len.saturating_sub(min_len) + 1;
186 let extract_len = min_len + ((input as usize) % len_range);
187 let max_offset = LOREM_IPSUM.len().saturating_sub(extract_len);
188 let h2 = xxh3_64(&input.to_le_bytes());
189 let offset = if max_offset > 0 {
190 (h2 as usize) % (max_offset + 1)
191 } else {
192 0
193 };
194 let end = (offset + extract_len).min(LOREM_IPSUM.len());
195 let start = LOREM_IPSUM.floor_char_boundary(offset);
197 let end = LOREM_IPSUM.ceil_char_boundary(end);
198 LOREM_IPSUM[start..end].to_string()
199}
200
201pub struct HashedLines(pub Vec<String>);
205
206impl polydat::derive_support::PolydatSetup for HashedLines {}
207
208impl HashedLines {
209 pub fn split_lines(text: &str) -> Self {
212 let lines: Vec<String> = text
213 .lines()
214 .map(|l| l.to_string())
215 .filter(|l| !l.is_empty())
216 .collect();
217 assert!(!lines.is_empty(), "text source must have at least one line");
218 Self(lines)
219 }
220}
221
222#[polydat::polydat_node(category = String)]
231fn hashed_line_to_string(
232 input: u64,
233 source: polydat::derive_support::Const<&str>,
234 #[poly_const(HashedLines::split_lines, from = source)] lines: &HashedLines,
235) -> String {
236 let _ = source;
237 let idx = (input as usize) % lines.0.len();
238 lines.0[idx].clone()
239}
240
241impl HashedLineToString {
242 pub fn names() -> Self {
244 Self::new(NAMES.to_string())
245 }
246 pub fn lastnames() -> Self {
248 Self::new(LASTNAMES.to_string())
249 }
250 pub fn careers() -> Self {
252 Self::new(CAREERS.to_string())
253 }
254 pub fn companies() -> Self {
256 Self::new(COMPANIES.to_string())
257 }
258}
259
260fn parse_charset(spec: &str) -> Vec<char> {
261 let mut chars = Vec::new();
262 let spec_chars: Vec<char> = spec.chars().collect();
263 let mut i = 0;
264 while i < spec_chars.len() {
265 if i + 2 < spec_chars.len() && spec_chars[i + 1] == '-' {
266 for c in spec_chars[i]..=spec_chars[i + 2] {
267 chars.push(c);
268 }
269 i += 3;
270 } else {
271 chars.push(spec_chars[i]);
272 i += 1;
273 }
274 }
275 chars
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use polydat::ast::{PolydatNode, Value};
282
283 #[test]
284 fn lorem_ipsum_bundled() {
285 assert!(LOREM_IPSUM.len() > 90_000, "lorem ipsum should be ~93KB");
286 assert!(LOREM_IPSUM.starts_with("Lorem ipsum"));
287 }
288
289 #[test]
290 fn names_bundled() {
291 assert!(!NAMES.is_empty());
292 assert!(NAMES.lines().count() > 10);
293 }
294
295 #[test]
296 fn random_range_bounded() {
297 let node = RandomRange::new(10, 20);
298 let mut out = [Value::None];
299 for _ in 0..1000 {
300 node.eval(&[], &mut out);
301 assert!((10..20).contains(&out[0].as_u64()));
302 }
303 }
304
305 #[test]
306 fn random_f64_bounded() {
307 let node = RandomF64::new(1.0, 5.0);
308 let mut out = [Value::None];
309 for _ in 0..1000 {
310 node.eval(&[], &mut out);
311 let v = out[0].as_f64();
312 assert!((1.0..5.0).contains(&v), "out of range: {v}");
313 }
314 }
315
316 #[test]
317 fn random_string_charset() {
318 let node = RandomString::alphanumeric(20);
319 let mut out = [Value::None];
320 node.eval(&[], &mut out);
321 assert_eq!(out[0].as_str().len(), 20);
322 assert!(out[0].as_str().chars().all(|c| c.is_ascii_alphanumeric()));
323 }
324
325 #[test]
326 fn hashed_lorem_extract_deterministic() {
327 let node = HashedLoremExtract::new(50, 100);
328 let mut out1 = [Value::None];
329 let mut out2 = [Value::None];
330 node.eval(&[Value::U64(42)], &mut out1);
331 node.eval(&[Value::U64(42)], &mut out2);
332 assert_eq!(out1[0].as_str(), out2[0].as_str());
333 }
334
335 #[test]
336 fn hashed_lorem_extract_size_range() {
337 let node = HashedLoremExtract::new(20, 50);
338 let mut out = [Value::None];
339 for i in 0..100u64 {
340 let h = xxh3_64(&i.to_le_bytes());
341 node.eval(&[Value::U64(h)], &mut out);
342 let len = out[0].as_str().len();
343 assert!((19..=55).contains(&len), "len={len}"); }
345 }
346
347 #[test]
348 fn hashed_lorem_extract_varies() {
349 let node = HashedLoremExtract::new(10, 20);
350 let mut out1 = [Value::None];
351 let mut out2 = [Value::None];
352 let h1 = xxh3_64(&0u64.to_le_bytes());
353 let h2 = xxh3_64(&1u64.to_le_bytes());
354 node.eval(&[Value::U64(h1)], &mut out1);
355 node.eval(&[Value::U64(h2)], &mut out2);
356 assert_ne!(out1[0].as_str(), out2[0].as_str());
357 }
358
359 #[test]
360 fn hashed_line_names() {
361 let node = HashedLineToString::names();
362 let mut out = [Value::None];
363 let h = xxh3_64(&42u64.to_le_bytes());
364 node.eval(&[Value::U64(h)], &mut out);
365 assert!(!out[0].as_str().is_empty());
366 }
367
368 #[test]
369 fn hashed_line_careers() {
370 let node = HashedLineToString::careers();
371 let mut out = [Value::None];
372 let h = xxh3_64(&42u64.to_le_bytes());
373 node.eval(&[Value::U64(h)], &mut out);
374 assert!(!out[0].as_str().is_empty());
375 }
376
377 #[test]
378 fn hashed_line_deterministic() {
379 let node = HashedLineToString::names();
380 let mut out1 = [Value::None];
381 let mut out2 = [Value::None];
382 node.eval(&[Value::U64(12345)], &mut out1);
383 node.eval(&[Value::U64(12345)], &mut out2);
384 assert_eq!(out1[0].as_str(), out2[0].as_str());
385 }
386}