use crate::ast::Value;
#[cfg(test)]
use crate::ast::{PolydatNode, PortType};
use crate::derive_support::{Const, PolydatSetup};
#[inline]
fn hash_to_unit(v: u64) -> f64 {
(v as f64) / ((u64::MAX as f64) + 1.0)
}
#[crate::polydat_node(category = Probability)]
fn fair_coin(input: u64) -> u64 {
let h = crate::library::hash::splitmix64_u64(input);
h & 1
}
#[crate::polydat_node(category = Probability)]
fn unfair_coin(input: u64, p: Const<f64>) -> u64 {
if !(0.0..=1.0).contains(&*p) {
panic!("unfair_coin probability p must be in [0.0, 1.0], got {}", *p);
}
let h = crate::library::hash::splitmix64_u64(input);
let unit = hash_to_unit(h);
if unit < *p { 1 } else { 0 }
}
#[crate::polydat_node(category = Probability)]
fn select(cond: u64, if_true: u64, if_false: u64) -> u64 {
if cond != 0 { if_true } else { if_false }
}
#[crate::polydat_node(category = Probability)]
fn chance(input: u64, p: Const<f64>) -> u64 {
if !(0.0..=1.0).contains(&*p) {
panic!("chance probability p must be in [0.0, 1.0], got {}", *p);
}
let h = crate::library::hash::splitmix64_u64(input);
let unit = hash_to_unit(h);
let result: f64 = if unit < *p { 1.0 } else { 0.0 };
result.to_bits()
}
#[crate::polydat_node(category = Probability)]
fn n_of(input: u64, n: Const<u64>, m: Const<u64>) -> u64 {
if *m == 0 {
panic!("n_of: m must be > 0");
}
if *n > *m {
panic!("n_of: n ({}) must be <= m ({})", *n, *m);
}
n_of_m_eval(input, *n, *m)
}
#[inline]
pub(crate) fn n_of_m_eval(input: u64, n: u64, m: u64) -> u64 {
let window = input / m;
let pos = input % m;
let my_hash = crate::library::hash::splitmix64_u64(window.wrapping_mul(0x517cc1b727220a95) ^ pos.wrapping_mul(0x9e3779b97f4a7c15));
let mut rank: u64 = 0;
for i in 0..m {
if i == pos {
continue;
}
let other_hash = crate::library::hash::splitmix64_u64(window.wrapping_mul(0x517cc1b727220a95) ^ i.wrapping_mul(0x9e3779b97f4a7c15));
if other_hash < my_hash || (other_hash == my_hash && i < pos) {
rank += 1;
}
}
if rank < n { 1 } else { 0 }
}
#[crate::polydat_node(category = Probability)]
fn one_of(input: u64, values: Const<Vec<String>>) -> String {
assert!(!values.is_empty(), "one_of: values must be non-empty");
let h = crate::library::hash::splitmix64_u64(input);
let idx = (h % values.len() as u64) as usize;
values[idx].clone()
}
pub struct WeightedTable {
pub values: Vec<String>,
pub cumulative: Vec<f64>,
}
impl PolydatSetup for WeightedTable {}
impl WeightedTable {
pub fn parse(spec: &str) -> Self {
let mut values = Vec::new();
let mut weights = Vec::new();
for elem in spec.split([';', ',']) {
let elem = elem.trim();
if elem.is_empty() { continue; }
let parts: Vec<&str> = elem.splitn(2, ':').collect();
assert_eq!(parts.len(), 2, "one_of_weighted: expected 'value:weight', got '{elem}'");
values.push(parts[0].to_string());
let w: f64 = parts[1].parse().expect("one_of_weighted: invalid weight");
assert!(w > 0.0, "one_of_weighted: weight must be positive, got {w}");
weights.push(w);
}
assert!(!values.is_empty(), "one_of_weighted: spec must be non-empty");
let total: f64 = weights.iter().sum();
assert!(total > 0.0, "one_of_weighted: total weight must be > 0");
let mut cumulative = Vec::with_capacity(weights.len());
let mut running = 0.0;
for w in &weights {
running += w / total;
cumulative.push(running);
}
if let Some(last) = cumulative.last_mut() {
*last = 1.0;
}
Self { values, cumulative }
}
}
#[crate::polydat_node(category = Probability)]
fn one_of_weighted(
input: u64,
spec: Const<&str>,
#[poly_const(WeightedTable::parse, from = spec)] table: &WeightedTable,
) -> String {
let _ = spec;
let h = crate::library::hash::splitmix64_u64(input);
let unit = hash_to_unit(h);
let idx = match table.cumulative.binary_search_by(|c| {
c.partial_cmp(&unit).unwrap()
}) {
Ok(i) => i,
Err(i) => i,
};
let idx = idx.min(table.values.len() - 1);
table.values[idx].clone()
}
#[crate::polydat_node(category = Probability)]
fn blend(a: u64, b: u64, mix: Const<f64>) -> u64 {
if !(0.0..=1.0).contains(&*mix) {
panic!("blend: mix must be in [0.0, 1.0], got {}", *mix);
}
let a_f = f64::from_bits(a);
let b_f = f64::from_bits(b);
let result = a_f * (1.0 - *mix) + b_f * *mix;
result.to_bits()
}
#[crate::polydat_node(category = Probability)]
fn default_or(value: Value, fallback: Value) -> Value {
if matches!(value, Value::None) { fallback } else { value }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fair_coin_returns_0_or_1() {
let node = FairCoin::new();
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
let v = out[0].as_u64();
assert!(v == 0 || v == 1, "fair_coin({i}) returned {v}, expected 0 or 1");
}
}
#[test]
fn fair_coin_deterministic() {
let node = FairCoin::new();
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_u64(), out2[0].as_u64());
}
#[test]
fn fair_coin_roughly_balanced() {
let node = FairCoin::new();
let mut out = [Value::None];
let mut ones = 0u64;
let n = 10_000u64;
for i in 0..n {
node.eval(&[Value::U64(i)], &mut out);
ones += out[0].as_u64();
}
let ratio = ones as f64 / n as f64;
assert!(
(0.45..=0.55).contains(&ratio),
"fair_coin ratio {ratio} outside expected 0.45-0.55"
);
}
#[test]
fn fair_coin_compiled_u64() {
let node = FairCoin::new();
let compiled = node.compiled_u64().expect("should have compiled_u64");
let inputs = [42u64];
let mut outputs = [0u64];
compiled(&inputs, &mut outputs);
assert!(outputs[0] == 0 || outputs[0] == 1);
let mut eval_out = [Value::None];
node.eval(&[Value::U64(42)], &mut eval_out);
assert_eq!(outputs[0], eval_out[0].as_u64());
}
#[test]
fn unfair_coin_always_0_when_p_is_0() {
let node = UnfairCoin::new(0.0);
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
assert_eq!(out[0].as_u64(), 0, "unfair_coin(p=0.0) should always return 0");
}
}
#[test]
fn unfair_coin_always_1_when_p_is_1() {
let node = UnfairCoin::new(1.0);
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
assert_eq!(out[0].as_u64(), 1, "unfair_coin(p=1.0) should always return 1");
}
}
#[test]
fn unfair_coin_respects_probability() {
let node = UnfairCoin::new(0.2);
let mut out = [Value::None];
let mut ones = 0u64;
let n = 10_000u64;
for i in 0..n {
node.eval(&[Value::U64(i)], &mut out);
ones += out[0].as_u64();
}
let ratio = ones as f64 / n as f64;
assert!(
(0.15..=0.25).contains(&ratio),
"unfair_coin(p=0.2) ratio {ratio} outside expected 0.15-0.25"
);
}
#[test]
fn unfair_coin_compiled_u64() {
let node = UnfairCoin::new(0.5);
let compiled = node.compiled_u64().expect("should have compiled_u64");
let inputs = [42u64];
let mut outputs = [0u64];
compiled(&inputs, &mut outputs);
assert!(outputs[0] == 0 || outputs[0] == 1);
let mut eval_out = [Value::None];
node.eval(&[Value::U64(42)], &mut eval_out);
assert_eq!(outputs[0], eval_out[0].as_u64());
}
#[test]
#[should_panic(expected = "unfair_coin probability p must be in [0.0, 1.0]")]
fn unfair_coin_rejects_invalid_p() {
let node = UnfairCoin::new(1.5);
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
}
#[test]
fn select_true_branch() {
let node = Select::new();
let mut out = [Value::None];
node.eval(&[Value::U64(1), Value::U64(100), Value::U64(200)], &mut out);
assert_eq!(out[0].as_u64(), 100);
}
#[test]
fn select_false_branch() {
let node = Select::new();
let mut out = [Value::None];
node.eval(&[Value::U64(0), Value::U64(100), Value::U64(200)], &mut out);
assert_eq!(out[0].as_u64(), 200);
}
#[test]
fn select_nonzero_is_true() {
let node = Select::new();
let mut out = [Value::None];
node.eval(&[Value::U64(999), Value::U64(10), Value::U64(20)], &mut out);
assert_eq!(out[0].as_u64(), 10);
}
#[test]
fn select_compiled_u64() {
let node = Select::new();
let compiled = node.compiled_u64().expect("should have compiled_u64");
let mut outputs = [0u64];
compiled(&[1, 100, 200], &mut outputs);
assert_eq!(outputs[0], 100);
compiled(&[0, 100, 200], &mut outputs);
assert_eq!(outputs[0], 200);
}
#[test]
fn chance_returns_f64_bits() {
let node = Chance::new(0.5);
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
let bits = out[0].as_u64();
let f = f64::from_bits(bits);
assert!(
f == 0.0 || f == 1.0,
"chance({i}) returned f64 {f}, expected 0.0 or 1.0"
);
}
}
#[test]
fn chance_always_0_when_p_is_0() {
let node = Chance::new(0.0);
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
let f = f64::from_bits(out[0].as_u64());
assert_eq!(f, 0.0);
}
}
#[test]
fn chance_always_1_when_p_is_1() {
let node = Chance::new(1.0);
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
let f = f64::from_bits(out[0].as_u64());
assert_eq!(f, 1.0);
}
}
#[test]
fn chance_compiled_u64() {
let node = Chance::new(0.5);
let compiled = node.compiled_u64().expect("should have compiled_u64");
let inputs = [42u64];
let mut outputs = [0u64];
compiled(&inputs, &mut outputs);
let f = f64::from_bits(outputs[0]);
assert!(f == 0.0 || f == 1.0);
let mut eval_out = [Value::None];
node.eval(&[Value::U64(42)], &mut eval_out);
assert_eq!(outputs[0], eval_out[0].as_u64());
}
#[test]
fn n_of_m_exact_count() {
let node = NOf::new(3, 10);
let mut out = [Value::None];
for window in 0..10u64 {
let mut count = 0u64;
for pos in 0..10u64 {
let input = window * 10 + pos;
node.eval(&[Value::U64(input)], &mut out);
count += out[0].as_u64();
}
assert_eq!(
count, 3,
"n_of(3, 10) window {window}: expected exactly 3 selected, got {count}"
);
}
}
#[test]
fn n_of_m_all_selected() {
let node = NOf::new(5, 5);
let mut out = [Value::None];
for i in 0..20u64 {
node.eval(&[Value::U64(i)], &mut out);
assert_eq!(out[0].as_u64(), 1, "n_of(5, 5) should always return 1");
}
}
#[test]
fn n_of_m_none_selected() {
let node = NOf::new(0, 5);
let mut out = [Value::None];
for i in 0..20u64 {
node.eval(&[Value::U64(i)], &mut out);
assert_eq!(out[0].as_u64(), 0, "n_of(0, 5) should always return 0");
}
}
#[test]
fn n_of_m_deterministic() {
let node = NOf::new(2, 7);
let mut out1 = [Value::None];
let mut out2 = [Value::None];
for i in 0..50u64 {
node.eval(&[Value::U64(i)], &mut out1);
node.eval(&[Value::U64(i)], &mut out2);
assert_eq!(out1[0].as_u64(), out2[0].as_u64());
}
}
#[test]
fn n_of_m_compiled_u64() {
let node = NOf::new(3, 10);
let compiled = node.compiled_u64().expect("should have compiled_u64");
for i in 0..10u64 {
let mut c_out = [0u64];
compiled(&[i], &mut c_out);
let mut e_out = [Value::None];
node.eval(&[Value::U64(i)], &mut e_out);
assert_eq!(c_out[0], e_out[0].as_u64(), "compiled/eval mismatch at input {i}");
}
}
#[test]
#[should_panic(expected = "n_of: m must be > 0")]
fn n_of_m_rejects_zero_m() {
let node = NOf::new(0, 0);
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
}
#[test]
#[should_panic(expected = "n_of: n (5) must be <= m (3)")]
fn n_of_m_rejects_n_greater_than_m() {
let node = NOf::new(5, 3);
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
}
#[test]
fn n_of_m_not_first_n() {
let node = NOf::new(1, 10);
let mut out = [Value::None];
let mut selected_positions = Vec::new();
for window in 0..20u64 {
for pos in 0..10u64 {
let input = window * 10 + pos;
node.eval(&[Value::U64(input)], &mut out);
if out[0].as_u64() == 1 {
selected_positions.push(pos);
}
}
}
let unique: std::collections::HashSet<u64> = selected_positions.iter().copied().collect();
assert!(
unique.len() > 1,
"n_of should select different positions across windows, got only {:?}",
unique
);
}
#[test]
fn one_of_selects_from_values() {
let node = OneOf::new(vec!["alpha".into(), "beta".into(), "gamma".into()]);
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
let s = out[0].as_str().to_string();
assert!(
s == "alpha" || s == "beta" || s == "gamma",
"one_of({i}) returned '{s}', expected one of alpha/beta/gamma"
);
}
}
#[test]
fn one_of_deterministic() {
let node = OneOf::new(vec!["x".into(), "y".into(), "z".into()]);
let mut out1 = [Value::None];
let mut out2 = [Value::None];
for i in 0..50u64 {
node.eval(&[Value::U64(i)], &mut out1);
node.eval(&[Value::U64(i)], &mut out2);
assert_eq!(out1[0].as_str(), out2[0].as_str());
}
}
#[test]
fn one_of_roughly_uniform() {
let values = vec!["a".into(), "b".into(), "c".into()];
let node = OneOf::new(values);
let mut out = [Value::None];
let mut counts = [0u64; 3];
let n = 9_000u64;
for i in 0..n {
node.eval(&[Value::U64(i)], &mut out);
match out[0].as_str() {
"a" => counts[0] += 1,
"b" => counts[1] += 1,
"c" => counts[2] += 1,
other => panic!("unexpected value: {other}"),
}
}
for (idx, &c) in counts.iter().enumerate() {
let ratio = c as f64 / n as f64;
assert!(
(0.25..=0.42).contains(&ratio),
"one_of bucket {idx} ratio {ratio} outside expected 0.25-0.42"
);
}
}
#[test]
fn one_of_single_value() {
let node = OneOf::new(vec!["only".into()]);
let mut out = [Value::None];
for i in 0..20u64 {
node.eval(&[Value::U64(i)], &mut out);
assert_eq!(out[0].as_str(), "only");
}
}
#[test]
#[should_panic(expected = "one_of: values must be non-empty")]
fn one_of_rejects_empty() {
let node = OneOf::new(vec![]);
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
}
#[test]
fn one_of_weighted_selects_from_spec() {
let node = OneOfWeighted::new("red:60,blue:30,green:10".to_string());
let mut out = [Value::None];
for i in 0..100u64 {
node.eval(&[Value::U64(i)], &mut out);
let s = out[0].as_str().to_string();
assert!(
s == "red" || s == "blue" || s == "green",
"one_of_weighted({i}) returned '{s}'"
);
}
}
#[test]
fn one_of_weighted_deterministic() {
let node = OneOfWeighted::new("a:50,b:50".to_string());
let mut out1 = [Value::None];
let mut out2 = [Value::None];
for i in 0..50u64 {
node.eval(&[Value::U64(i)], &mut out1);
node.eval(&[Value::U64(i)], &mut out2);
assert_eq!(out1[0].as_str(), out2[0].as_str());
}
}
#[test]
fn one_of_weighted_respects_weights() {
let node = OneOfWeighted::new("heavy:90,light:10".to_string());
let mut out = [Value::None];
let mut heavy = 0u64;
let n = 10_000u64;
for i in 0..n {
node.eval(&[Value::U64(i)], &mut out);
if out[0].as_str() == "heavy" {
heavy += 1;
}
}
let ratio = heavy as f64 / n as f64;
assert!(
(0.80..=0.97).contains(&ratio),
"one_of_weighted heavy ratio {ratio} outside expected 0.80-0.97"
);
}
#[test]
fn one_of_weighted_single_value() {
let node = OneOfWeighted::new("only:1".to_string());
let mut out = [Value::None];
for i in 0..20u64 {
node.eval(&[Value::U64(i)], &mut out);
assert_eq!(out[0].as_str(), "only");
}
}
#[test]
fn one_of_weighted_semicolon_delimiter() {
let node = OneOfWeighted::new("x:50;y:50".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(0)], &mut out);
let s = out[0].as_str().to_string();
assert!(s == "x" || s == "y");
}
#[test]
#[should_panic(expected = "one_of_weighted: spec must be non-empty")]
fn one_of_weighted_rejects_empty() {
OneOfWeighted::new("".to_string());
}
#[test]
#[should_panic(expected = "one_of_weighted: expected 'value:weight'")]
fn one_of_weighted_rejects_bad_format() {
OneOfWeighted::new("noweight".to_string());
}
#[test]
fn blend_pure_a_when_mix_is_0() {
let node = Blend::new(0.0);
let a: f64 = 10.0;
let b: f64 = 20.0;
let mut out = [Value::None];
node.eval(
&[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
&mut out,
);
let result = f64::from_bits(out[0].as_u64());
assert!((result - 10.0).abs() < 1e-10, "blend(mix=0) should return a, got {result}");
}
#[test]
fn blend_pure_b_when_mix_is_1() {
let node = Blend::new(1.0);
let a: f64 = 10.0;
let b: f64 = 20.0;
let mut out = [Value::None];
node.eval(
&[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
&mut out,
);
let result = f64::from_bits(out[0].as_u64());
assert!((result - 20.0).abs() < 1e-10, "blend(mix=1) should return b, got {result}");
}
#[test]
fn blend_half_mix() {
let node = Blend::new(0.5);
let a: f64 = 10.0;
let b: f64 = 20.0;
let mut out = [Value::None];
node.eval(
&[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
&mut out,
);
let result = f64::from_bits(out[0].as_u64());
assert!(
(result - 15.0).abs() < 1e-10,
"blend(mix=0.5) of 10.0 and 20.0 should be 15.0, got {result}"
);
}
#[test]
fn blend_quarter_mix() {
let node = Blend::new(0.25);
let a: f64 = 0.0;
let b: f64 = 100.0;
let mut out = [Value::None];
node.eval(
&[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
&mut out,
);
let result = f64::from_bits(out[0].as_u64());
assert!(
(result - 25.0).abs() < 1e-10,
"blend(mix=0.25) of 0.0 and 100.0 should be 25.0, got {result}"
);
}
#[test]
fn blend_compiled_u64() {
let node = Blend::new(0.5);
let compiled = node.compiled_u64().expect("should have compiled_u64");
let a: f64 = 10.0;
let b: f64 = 20.0;
let inputs = [a.to_bits(), b.to_bits()];
let mut outputs = [0u64];
compiled(&inputs, &mut outputs);
let result = f64::from_bits(outputs[0]);
assert!((result - 15.0).abs() < 1e-10);
let mut eval_out = [Value::None];
node.eval(
&[Value::U64(a.to_bits()), Value::U64(b.to_bits())],
&mut eval_out,
);
assert_eq!(outputs[0], eval_out[0].as_u64());
}
#[test]
#[should_panic(expected = "blend: mix must be in [0.0, 1.0]")]
fn blend_rejects_invalid_mix() {
let node = Blend::new(1.5);
let mut out = [Value::None];
node.eval(&[Value::U64(0), Value::U64(0)], &mut out);
}
#[test]
#[should_panic(expected = "blend: mix must be in [0.0, 1.0]")]
fn blend_rejects_negative_mix() {
let node = Blend::new(-0.1);
let mut out = [Value::None];
node.eval(&[Value::U64(0), Value::U64(0)], &mut out);
}
#[test]
fn default_or_returns_value_when_not_none() {
let node = DefaultOr::new(PortType::Str, PortType::Str);
let mut out = [Value::None];
node.eval(&[Value::Str("alice".into()), Value::Str("fallback".into())], &mut out);
assert_eq!(out[0].as_str(), "alice");
}
#[test]
fn default_or_returns_fallback_when_none() {
let node = DefaultOr::new(PortType::Str, PortType::Str);
let mut out = [Value::None];
node.eval(&[Value::None, Value::Str("fallback".into())], &mut out);
assert_eq!(out[0].as_str(), "fallback");
}
#[test]
fn default_or_works_with_u64() {
let node = DefaultOr::new(PortType::U64, PortType::U64);
let mut out = [Value::None];
node.eval(&[Value::U64(42), Value::U64(0)], &mut out);
assert!(matches!(out[0], Value::U64(42)));
node.eval(&[Value::None, Value::U64(99)], &mut out);
assert!(matches!(out[0], Value::U64(99)));
}
#[test]
fn default_or_with_extern_input() {
use crate::compile::assembly::{PolydatAssembler, WireRef};
use crate::library::identity::PortPassthrough;
let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
asm.add_input("captured_name", Value::None, PortType::Str, crate::kernel::InputKind::ExternalWrite);
asm.add_node("__port_captured_name",
Box::new(PortPassthrough::new("captured_name", PortType::Str)),
vec![WireRef::input("captured_name")]);
asm.add_node("fallback",
Box::new(crate::library::identity::ConstStr::new("anonymous".to_string())),
vec![]);
asm.add_node("greeting",
Box::new(DefaultOr::new(PortType::Str, PortType::Str)),
vec![WireRef::node("__port_captured_name"), WireRef::node("fallback")]);
asm.add_output("greeting", WireRef::node("greeting"));
let kernel = asm.compile().unwrap();
let program = kernel.into_program();
let mut state = program.create_state();
state.set_inputs(&[0]);
let val = state.pull(&program, "greeting");
assert_eq!(val.to_display_string(), "anonymous",
"unset extern should produce fallback, got: {:?}", val);
let input_idx = program.find_input("captured_name").unwrap();
state.set_input(input_idx, Value::Str("alice".into()));
let val = state.pull(&program, "greeting");
assert_eq!(val.to_display_string(), "alice",
"set extern should produce captured value, got: {:?}", val);
state.reset_inputs_from(program.coord_count());
let val = state.pull(&program, "greeting");
assert_eq!(val.to_display_string(), "anonymous",
"reset extern should produce fallback again, got: {:?}", val);
}
}