pub struct ParsedCombinations {
pub segments: Vec<Segment>,
pub modulus: u64,
}
impl crate::derive_support::PolydatSetup for ParsedCombinations {}
pub enum Segment {
Charset(Vec<char>),
Literal(String),
}
impl ParsedCombinations {
pub fn from_pattern(pattern: &str) -> Self {
let mut segments = Vec::new();
let mut modulus: u64 = 1;
for spec in pattern.split(';') {
let chars = parse_charset(spec);
if chars.len() == 1 && !spec.contains('-') {
segments.push(Segment::Literal(chars[0].to_string()));
} else if chars.is_empty() {
segments.push(Segment::Literal(spec.to_string()));
} else {
modulus = modulus.saturating_mul(chars.len() as u64);
segments.push(Segment::Charset(chars));
}
}
Self { segments, modulus }
}
}
#[crate::polydat_node(category = String)]
fn combinations(
input: u64,
pattern: crate::derive_support::Const<&str>,
#[poly_const(ParsedCombinations::from_pattern, from = pattern)]
parsed: &ParsedCombinations,
) -> String {
let mut remainder = if parsed.modulus > 0 { input % parsed.modulus } else { input };
let mut result = String::with_capacity(parsed.segments.len() * 2);
for seg in &parsed.segments {
match seg {
Segment::Literal(s) => result.push_str(s),
Segment::Charset(chars) => {
let radix = chars.len() as u64;
if radix > 0 {
let idx = (remainder % radix) as usize;
result.push(chars[idx]);
remainder /= radix;
}
}
}
}
result
}
impl Combinations {
pub fn cardinality(&self) -> u64 {
self.parsed.modulus
}
}
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] == '-' {
let start = spec_chars[i];
let end = spec_chars[i + 2];
for c in start..=end {
chars.push(c);
}
i += 3;
} else {
chars.push(spec_chars[i]);
i += 1;
}
}
chars
}
#[crate::polydat_node(category = String)]
fn number_to_words(input: u64) -> String {
u64_to_words(input)
}
const ONES: [&str; 20] = [
"zero", "one", "two", "three", "four", "five", "six", "seven",
"eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen",
"fifteen", "sixteen", "seventeen", "eighteen", "nineteen",
];
const TENS: [&str; 10] = [
"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy",
"eighty", "ninety",
];
const SCALES: [&str; 7] = [
"", "thousand", "million", "billion", "trillion", "quadrillion",
"quintillion",
];
fn u64_to_words(n: u64) -> String {
if n < 20 {
return ONES[n as usize].to_string();
}
let mut buf = String::with_capacity(64);
let mut chunks = [0u32; 7];
let mut num_chunks = 0;
let mut remaining = n;
while remaining > 0 {
chunks[num_chunks] = (remaining % 1000) as u32;
num_chunks += 1;
remaining /= 1000;
}
let mut first = true;
for i in (0..num_chunks).rev() {
let chunk = chunks[i];
if chunk > 0 {
if !first {
buf.push(' ');
}
first = false;
append_chunk_to_words(&mut buf, chunk);
if i > 0 && i < SCALES.len() {
buf.push(' ');
buf.push_str(SCALES[i]);
}
}
}
buf
}
fn append_chunk_to_words(buf: &mut String, n: u32) {
let hundreds = n / 100;
let remainder = n % 100;
let mut has_hundreds = false;
if hundreds > 0 {
buf.push_str(ONES[hundreds as usize]);
buf.push_str(" hundred");
has_hundreds = true;
}
if remainder >= 20 {
if has_hundreds {
buf.push(' ');
}
let tens = remainder / 10;
let ones = remainder % 10;
buf.push_str(TENS[tens as usize]);
if ones > 0 {
buf.push('-');
buf.push_str(ONES[ones as usize]);
}
} else if remainder > 0 {
if has_hundreds {
buf.push(' ');
}
buf.push_str(ONES[remainder as usize]);
}
}
use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
use crate::ast::SlotType;
pub fn signatures() -> &'static [FuncSig] {
use FuncCategory as C;
&[
FuncSig {
name: "file_line_at", category: C::String, outputs: 1,
description: "select a line from a file by index",
help: "Read a file at construction time and return a line at cycle-time index.\nIndex wraps modulo line count so every u64 input is valid.\nFile path is a const string argument.\nParameters:\n index — u64 wire input\n filename — ConstStr path to file\nExample: file_line_at(mod(hash(cycle), 1000), \"words.txt\")",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "index", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "filename", slot_type: SlotType::ConstStr, required: true, example: "\"test.csv\"", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::ast::Commutativity::Positional,
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
output_port: None,
},
]
}
#[crate::polydat_node(category = String)]
fn hashed_uuid(input: u64) -> String {
let h1 = xxhash_rust::xxh3::xxh3_64(&input.to_le_bytes());
let h2 = xxhash_rust::xxh3::xxh3_64(&h1.to_le_bytes());
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&h1.to_le_bytes());
bytes[8..].copy_from_slice(&h2.to_le_bytes());
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5],
bytes[6], bytes[7],
bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
)
}
fn expand_charset(charset: &str) -> Vec<char> {
if charset.is_empty() {
return ('a'..='z').collect();
}
let mut result = Vec::new();
let chars_vec: Vec<char> = charset.chars().collect();
let mut i = 0;
while i < chars_vec.len() {
if i + 2 < chars_vec.len() && chars_vec[i + 1] == '-' {
for c in chars_vec[i]..=chars_vec[i + 2] { result.push(c); }
i += 3;
} else {
result.push(chars_vec[i]);
i += 1;
}
}
if result.is_empty() { ('a'..='z').collect() } else { result }
}
#[crate::polydat_node(category = String)]
fn char_buf(
seed: u64,
charset: crate::derive_support::Const<&str>,
length: u64,
#[poly_const(expand_charset, from = charset)]
chars: &Vec<char>,
) -> String {
let n = chars.len();
let len = length as usize;
if n == 0 || len == 0 {
return String::new();
}
let mut result = String::with_capacity(len);
let mut h = seed;
for _ in 0..len {
h = xxhash_rust::xxh3::xxh3_64(&h.to_le_bytes());
result.push(chars[(h as usize) % n]);
}
result
}
impl crate::derive_support::PolydatSetup for Vec<String> {}
fn read_file_lines(filename: &str) -> Vec<String> {
let content = std::fs::read_to_string(filename)
.unwrap_or_else(|e| panic!("failed to read file '{filename}': {e}"));
let lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
if lines.is_empty() {
panic!("file '{filename}' has no lines");
}
lines
}
#[crate::polydat_node(category = String)]
fn file_line_at(
index: u64,
filename: crate::derive_support::Const<&str>,
#[poly_const(read_file_lines, from = filename)]
lines: &Vec<String>,
) -> String {
let _ = filename;
let idx = index as usize;
lines[idx % lines.len()].clone()
}
#[crate::polydat_node(category = String)]
fn str_concat(parts: &[polydat::ast::Value]) -> String {
use polydat::ast::Value;
let mut out = String::new();
for v in parts {
match v {
Value::Str(s) => out.push_str(s),
Value::U64(n) => out.push_str(&n.to_string()),
Value::F64(n) => out.push_str(&n.to_string()),
Value::Bool(b) => out.push_str(&b.to_string()),
Value::Json(j) => out.push_str(&j.to_string()),
Value::Bytes(b) => out.push_str(&String::from_utf8_lossy(b)),
other => out.push_str(&format!("{other:?}")),
}
}
out
}
#[crate::polydat_node(category = String)]
fn str_lower(input: String) -> String {
input.to_lowercase()
}
#[crate::polydat_node(category = String)]
fn str_upper(input: String) -> String {
input.to_uppercase()
}
pub(crate) fn build_node(_name: &str, _wires: &[crate::compile::assembly::WireRef], _wire_types: &[crate::ast::PortType], _consts: &[crate::dsl::factory::ConstArg]) -> Option<Result<Box<dyn crate::ast::PolydatNode>, String>> {
None
}
crate::register_nodes!(signatures, build_node);
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PolydatNode, Value};
#[test]
fn combinations_digits() {
let node = Combinations::new("0-9;0-9;0-9".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(123)], &mut out);
let s = out[0].as_str();
assert_eq!(s.len(), 3);
assert!(s.chars().all(|c| c.is_ascii_digit()));
}
#[test]
fn combinations_with_separator() {
let node = Combinations::new("0-9;0-9;0-9;-;0-9;0-9;0-9".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
let s = out[0].as_str();
assert_eq!(s.len(), 7); assert_eq!(&s[3..4], "-");
}
#[test]
fn combinations_alpha() {
let node = Combinations::new("A-Z;A-Z;A-Z".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
assert_eq!(out[0].as_str(), "AAA");
node.eval(&[Value::U64(1)], &mut out);
assert_eq!(out[0].as_str(), "BAA");
}
#[test]
fn combinations_cardinality() {
let node = Combinations::new("0-9;0-9;-;A-Z".to_string());
assert_eq!(node.cardinality(), 2600);
}
#[test]
fn combinations_deterministic() {
let node = Combinations::new("A-Z;0-9".to_string());
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 combinations_wraps() {
let node = Combinations::new("0-9".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
let a = out[0].as_str().to_string();
node.eval(&[Value::U64(10)], &mut out);
assert_eq!(out[0].as_str(), &a, "should wrap at cardinality");
}
#[test]
fn number_to_words_zero() {
assert_eq!(u64_to_words(0), "zero");
}
#[test]
fn number_to_words_teens() {
assert_eq!(u64_to_words(1), "one");
assert_eq!(u64_to_words(11), "eleven");
assert_eq!(u64_to_words(19), "nineteen");
}
#[test]
fn number_to_words_tens() {
assert_eq!(u64_to_words(20), "twenty");
assert_eq!(u64_to_words(42), "forty-two");
assert_eq!(u64_to_words(99), "ninety-nine");
}
#[test]
fn number_to_words_hundreds() {
assert_eq!(u64_to_words(100), "one hundred");
assert_eq!(u64_to_words(123), "one hundred twenty-three");
assert_eq!(u64_to_words(500), "five hundred");
}
#[test]
fn number_to_words_thousands() {
assert_eq!(u64_to_words(1000), "one thousand");
assert_eq!(u64_to_words(1001), "one thousand one");
assert_eq!(u64_to_words(12345), "twelve thousand three hundred forty-five");
}
#[test]
fn number_to_words_millions() {
assert_eq!(u64_to_words(1_000_000), "one million");
assert_eq!(
u64_to_words(1_234_567),
"one million two hundred thirty-four thousand five hundred sixty-seven"
);
}
#[test]
fn number_to_words_large() {
let s = u64_to_words(1_000_000_000_000);
assert!(s.starts_with("one trillion"), "got: {s}");
}
#[test]
fn number_to_words_node() {
let node = NumberToWords::new();
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_str(), "forty-two");
}
#[test]
fn str_concat_basic() {
let node = StrConcat::new(2);
let mut out = [Value::None];
node.eval(
&[Value::Str("hello ".into()), Value::Str("world".into())],
&mut out,
);
assert_eq!(out[0].as_str(), "hello world");
}
#[test]
fn str_concat_mixed_types() {
let node = StrConcat::new(4);
let mut out = [Value::None];
node.eval(
&[
Value::Str("id=".into()),
Value::U64(42),
Value::Str(" v=".into()),
Value::F64(3.14),
],
&mut out,
);
assert_eq!(out[0].as_str(), "id=42 v=3.14");
}
#[test]
fn str_concat_empty() {
let node = StrConcat::new(0);
let mut out = [Value::None];
node.eval(&[], &mut out);
assert_eq!(out[0].as_str(), "");
}
#[test]
fn str_lower_ascii_and_unicode() {
let node = StrLower::new();
let mut out = [Value::None];
node.eval(&[Value::Str("OTHER_M8".into())], &mut out);
assert_eq!(out[0].as_str(), "other_m8");
node.eval(&[Value::Str("ÄPFEL".into())], &mut out);
assert_eq!(out[0].as_str(), "äpfel");
}
#[test]
fn str_lower_idempotent_on_already_lowercase() {
let node = StrLower::new();
let mut out = [Value::None];
node.eval(&[Value::Str("fknn_oat_other".into())], &mut out);
assert_eq!(out[0].as_str(), "fknn_oat_other");
}
#[test]
fn str_upper_ascii_and_unicode() {
let node = StrUpper::new();
let mut out = [Value::None];
node.eval(&[Value::Str("other_m8".into())], &mut out);
assert_eq!(out[0].as_str(), "OTHER_M8");
node.eval(&[Value::Str("äpfel".into())], &mut out);
assert_eq!(out[0].as_str(), "ÄPFEL");
}
}