use std::cell::RefCell;
use xxhash_rust::xxh3::xxh3_64;
pub static LOREM_IPSUM: &str = include_str!("../../data/lorem_ipsum_full.txt");
pub static NAMES: &str = include_str!("../../data/names.txt");
pub static LASTNAMES: &str = include_str!("../../data/lastnames.txt");
pub static CAREERS: &str = include_str!("../../data/careers.txt");
pub static COMPANIES: &str = include_str!("../../data/companies.txt");
pub static VARIABLE_WORDS: &str = include_str!("../../data/variable_words.txt");
thread_local! {
static RNG: RefCell<u64> = RefCell::new(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64
);
}
fn next_u64() -> u64 {
RNG.with(|r| {
let mut s = *r.borrow();
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
*r.borrow_mut() = s;
s
})
}
fn next_f64() -> f64 {
next_u64() as f64 / u64::MAX as f64
}
#[crate::polydat_node(
category = Probability,
purity = Nondeterministic("thread-local PRNG"),
)]
fn random_range(
#[poly_default(0u64)] min: crate::derive_support::Const<u64>,
#[poly_default(100u64)] max: crate::derive_support::Const<u64>,
) -> u64 {
let range = max.saturating_sub(*min).max(1);
*min + (next_u64() % range)
}
#[crate::polydat_node(
category = Probability,
purity = Nondeterministic("thread-local PRNG"),
)]
fn random_f64(
#[poly_default(0.0f64)] min: crate::derive_support::Const<f64>,
#[poly_default(1.0f64)] max: crate::derive_support::Const<f64>,
) -> f64 {
*min + next_f64() * (*max - *min)
}
#[crate::polydat_node(
category = Probability,
purity = Nondeterministic("thread-local PRNG"),
)]
fn random_bytes(
#[poly_default(8u64)] size: crate::derive_support::Const<u64>,
) -> Vec<u8> {
let sz = *size as usize;
let mut buf = Vec::with_capacity(sz);
while buf.len() < sz {
let take = (sz - buf.len()).min(8);
buf.extend_from_slice(&next_u64().to_le_bytes()[..take]);
}
buf
}
#[crate::polydat_node(
category = Probability,
purity = Nondeterministic("thread-local PRNG"),
)]
fn random_string(
#[poly_default("A-Za-z0-9")] charset: crate::derive_support::Const<&str>,
#[poly_default(8u64)] length: crate::derive_support::Const<u64>,
) -> String {
let chars = parse_charset(&charset);
if chars.is_empty() {
return String::new();
}
(0..*length)
.map(|_| chars[(next_u64() as usize) % chars.len()])
.collect()
}
#[crate::polydat_node(
category = Probability,
purity = Nondeterministic("thread-local PRNG"),
)]
fn random_bool(
#[poly_default(0.5f64)] probability: crate::derive_support::Const<f64>,
) -> bool {
let threshold = (probability.clamp(0.0, 1.0) * u64::MAX as f64) as u64;
next_u64() < threshold
}
impl RandomString {
pub fn alphanumeric(length: u64) -> Self {
Self::new("A-Za-z0-9".to_string(), length)
}
}
#[crate::polydat_node(category = String)]
fn hashed_lorem_extract(
input: u64,
min_len: crate::derive_support::Const<u64>,
max_len: crate::derive_support::Const<u64>,
) -> String {
let min_len = *min_len as usize;
let max_len = *max_len as usize;
let len_range = max_len.saturating_sub(min_len) + 1;
let extract_len = min_len + ((input as usize) % len_range);
let max_offset = LOREM_IPSUM.len().saturating_sub(extract_len);
let h2 = xxh3_64(&input.to_le_bytes());
let offset = if max_offset > 0 { (h2 as usize) % (max_offset + 1) } else { 0 };
let end = (offset + extract_len).min(LOREM_IPSUM.len());
let start = LOREM_IPSUM.floor_char_boundary(offset);
let end = LOREM_IPSUM.ceil_char_boundary(end);
LOREM_IPSUM[start..end].to_string()
}
pub struct HashedLines(pub Vec<String>);
impl crate::derive_support::PolydatSetup for HashedLines {}
impl HashedLines {
pub fn split_lines(text: &str) -> Self {
let lines: Vec<String> = text
.lines()
.map(|l| l.to_string())
.filter(|l| !l.is_empty())
.collect();
assert!(!lines.is_empty(), "text source must have at least one line");
Self(lines)
}
}
#[crate::polydat_node(category = String)]
fn hashed_line_to_string(
input: u64,
source: crate::derive_support::Const<&str>,
#[poly_const(HashedLines::split_lines, from = source)]
lines: &HashedLines,
) -> String {
let _ = source;
let idx = (input as usize) % lines.0.len();
lines.0[idx].clone()
}
impl HashedLineToString {
pub fn names() -> Self { Self::new(NAMES.to_string()) }
pub fn lastnames() -> Self { Self::new(LASTNAMES.to_string()) }
pub fn careers() -> Self { Self::new(CAREERS.to_string()) }
pub fn companies() -> Self { Self::new(COMPANIES.to_string()) }
}
fn parse_charset(spec: &str) -> Vec<char> {
let mut chars = Vec::new();
let spec_chars: Vec<char> = spec.chars().collect();
let mut i = 0;
while i < spec_chars.len() {
if i + 2 < spec_chars.len() && spec_chars[i + 1] == '-' {
for c in spec_chars[i]..=spec_chars[i + 2] { chars.push(c); }
i += 3;
} else {
chars.push(spec_chars[i]);
i += 1;
}
}
chars
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PolydatNode, Value};
#[test]
fn lorem_ipsum_bundled() {
assert!(LOREM_IPSUM.len() > 90_000, "lorem ipsum should be ~93KB");
assert!(LOREM_IPSUM.starts_with("Lorem ipsum"));
}
#[test]
fn names_bundled() {
assert!(!NAMES.is_empty());
assert!(NAMES.lines().count() > 10);
}
#[test]
fn random_range_bounded() {
let node = RandomRange::new(10, 20);
let mut out = [Value::None];
for _ in 0..1000 {
node.eval(&[], &mut out);
assert!((10..20).contains(&out[0].as_u64()));
}
}
#[test]
fn random_f64_bounded() {
let node = RandomF64::new(1.0, 5.0);
let mut out = [Value::None];
for _ in 0..1000 {
node.eval(&[], &mut out);
let v = out[0].as_f64();
assert!((1.0..5.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn random_string_charset() {
let node = RandomString::alphanumeric(20);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str().len(), 20);
assert!(out[0].as_str().chars().all(|c| c.is_ascii_alphanumeric()));
}
#[test]
fn hashed_lorem_extract_deterministic() {
let node = HashedLoremExtract::new(50, 100);
let mut out1 = [Value::None];
let mut out2 = [Value::None];
node.eval(&[Value::U64(42)], &mut out1);
node.eval(&[Value::U64(42)], &mut out2);
assert_eq!(out1[0].as_str(), out2[0].as_str());
}
#[test]
fn hashed_lorem_extract_size_range() {
let node = HashedLoremExtract::new(20, 50);
let mut out = [Value::None];
for i in 0..100u64 {
let h = xxh3_64(&i.to_le_bytes());
node.eval(&[Value::U64(h)], &mut out);
let len = out[0].as_str().len();
assert!((19..=55).contains(&len), "len={len}"); }
}
#[test]
fn hashed_lorem_extract_varies() {
let node = HashedLoremExtract::new(10, 20);
let mut out1 = [Value::None];
let mut out2 = [Value::None];
let h1 = xxh3_64(&0u64.to_le_bytes());
let h2 = xxh3_64(&1u64.to_le_bytes());
node.eval(&[Value::U64(h1)], &mut out1);
node.eval(&[Value::U64(h2)], &mut out2);
assert_ne!(out1[0].as_str(), out2[0].as_str());
}
#[test]
fn hashed_line_names() {
let node = HashedLineToString::names();
let mut out = [Value::None];
let h = xxh3_64(&42u64.to_le_bytes());
node.eval(&[Value::U64(h)], &mut out);
assert!(!out[0].as_str().is_empty());
}
#[test]
fn hashed_line_careers() {
let node = HashedLineToString::careers();
let mut out = [Value::None];
let h = xxh3_64(&42u64.to_le_bytes());
node.eval(&[Value::U64(h)], &mut out);
assert!(!out[0].as_str().is_empty());
}
#[test]
fn hashed_line_deterministic() {
let node = HashedLineToString::names();
let mut out1 = [Value::None];
let mut out2 = [Value::None];
node.eval(&[Value::U64(12345)], &mut out1);
node.eval(&[Value::U64(12345)], &mut out2);
assert_eq!(out1[0].as_str(), out2[0].as_str());
}
}