use crate::node::{CompiledU64Op, GkNode, NodeMeta, Port, PortType, Slot, Value};
use xxhash_rust::xxh3::xxh3_64;
#[inline]
fn hash_to_unit(v: u64) -> f64 {
(v as f64) / ((u64::MAX as f64) + 1.0)
}
pub struct FairCoin {
meta: NodeMeta,
}
impl Default for FairCoin {
fn default() -> Self {
Self::new()
}
}
impl FairCoin {
pub fn new() -> Self {
Self {
meta: NodeMeta {
name: "fair_coin".into(),
outs: vec![Port::u64("output")],
ins: vec![Slot::Wire(Port::u64("input"))],
},
}
}
}
impl GkNode for FairCoin {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let h = xxh3_64(&inputs[0].as_u64().to_le_bytes());
outputs[0] = Value::U64(h % 2);
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
Some(Box::new(|inputs, outputs| {
let h = xxh3_64(&inputs[0].to_le_bytes());
outputs[0] = h % 2;
}))
}
}
pub struct UnfairCoin {
meta: NodeMeta,
p: f64,
}
impl UnfairCoin {
pub fn new(p: f64) -> Self {
assert!(
(0.0..=1.0).contains(&p),
"unfair_coin probability p must be in [0.0, 1.0], got {p}"
);
Self {
meta: NodeMeta {
name: "unfair_coin".into(),
outs: vec![Port::u64("output")],
ins: vec![
Slot::Wire(Port::u64("input")),
Slot::const_f64("p", p),
],
},
p,
}
}
}
impl GkNode for UnfairCoin {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let h = xxh3_64(&inputs[0].as_u64().to_le_bytes());
let unit = hash_to_unit(h);
outputs[0] = Value::U64(if unit < self.p { 1 } else { 0 });
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
let p = self.p;
Some(Box::new(move |inputs, outputs| {
let h = xxh3_64(&inputs[0].to_le_bytes());
let unit = hash_to_unit(h);
outputs[0] = if unit < p { 1 } else { 0 };
}))
}
fn jit_constants(&self) -> Vec<u64> { vec![self.p.to_bits()] }
}
pub struct Select {
meta: NodeMeta,
}
impl Default for Select {
fn default() -> Self {
Self::new()
}
}
impl Select {
pub fn new() -> Self {
Self {
meta: NodeMeta {
name: "select".into(),
outs: vec![Port::u64("output")],
ins: vec![
Slot::Wire(Port::u64("cond")),
Slot::Wire(Port::u64("if_true")),
Slot::Wire(Port::u64("if_false")),
],
},
}
}
}
impl GkNode for Select {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let cond = inputs[0].as_u64();
outputs[0] = if cond != 0 {
Value::U64(inputs[1].as_u64())
} else {
Value::U64(inputs[2].as_u64())
};
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
Some(Box::new(|inputs, outputs| {
outputs[0] = if inputs[0] != 0 { inputs[1] } else { inputs[2] };
}))
}
}
pub struct Chance {
meta: NodeMeta,
p: f64,
}
impl Chance {
pub fn new(p: f64) -> Self {
assert!(
(0.0..=1.0).contains(&p),
"chance probability p must be in [0.0, 1.0], got {p}"
);
Self {
meta: NodeMeta {
name: "chance".into(),
outs: vec![Port::u64("output")],
ins: vec![
Slot::Wire(Port::u64("input")),
Slot::const_f64("p", p),
],
},
p,
}
}
}
impl GkNode for Chance {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let h = xxh3_64(&inputs[0].as_u64().to_le_bytes());
let unit = hash_to_unit(h);
let result: f64 = if unit < self.p { 1.0 } else { 0.0 };
outputs[0] = Value::U64(result.to_bits());
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
let p = self.p;
Some(Box::new(move |inputs, outputs| {
let h = xxh3_64(&inputs[0].to_le_bytes());
let unit = hash_to_unit(h);
let result: f64 = if unit < p { 1.0 } else { 0.0 };
outputs[0] = result.to_bits();
}))
}
fn jit_constants(&self) -> Vec<u64> { vec![self.p.to_bits()] }
}
pub struct NofM {
meta: NodeMeta,
n: u64,
m: u64,
}
impl NofM {
pub fn new(n: u64, m: u64) -> Self {
assert!(m > 0, "n_of: m must be > 0");
assert!(n <= m, "n_of: n ({n}) must be <= m ({m})");
Self {
meta: NodeMeta {
name: "n_of".into(),
outs: vec![Port::u64("output")],
ins: vec![
Slot::Wire(Port::u64("input")),
Slot::const_u64("n", n),
Slot::const_u64("m", m),
],
},
n,
m,
}
}
}
impl GkNode for NofM {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let input = inputs[0].as_u64();
outputs[0] = Value::U64(n_of_m_eval(input, self.n, self.m));
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
let n = self.n;
let m = self.m;
Some(Box::new(move |inputs, outputs| {
outputs[0] = n_of_m_eval(inputs[0], n, m);
}))
}
fn jit_constants(&self) -> Vec<u64> { vec![self.n, self.m] }
}
#[inline]
fn n_of_m_eval(input: u64, n: u64, m: u64) -> u64 {
let window = input / m;
let pos = input % m;
let my_hash = xxh3_64(&[window.to_le_bytes(), pos.to_le_bytes()].concat());
let mut rank: u64 = 0;
for i in 0..m {
if i == pos {
continue;
}
let other_hash = xxh3_64(&[window.to_le_bytes(), i.to_le_bytes()].concat());
if other_hash < my_hash || (other_hash == my_hash && i < pos) {
rank += 1;
}
}
if rank < n { 1 } else { 0 }
}
pub struct OneOf {
meta: NodeMeta,
values: Vec<String>,
}
impl OneOf {
pub fn new(values: Vec<String>) -> Self {
assert!(!values.is_empty(), "one_of: values must be non-empty");
Self {
meta: NodeMeta {
name: "one_of".into(),
outs: vec![Port::str("output")],
ins: vec![Slot::Wire(Port::u64("input"))],
},
values,
}
}
}
impl GkNode for OneOf {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let h = xxh3_64(&inputs[0].as_u64().to_le_bytes());
let idx = (h % self.values.len() as u64) as usize;
outputs[0] = Value::Str(self.values[idx].clone().into());
}
}
pub struct OneOfWeighted {
meta: NodeMeta,
values: Vec<String>,
cumulative: Vec<f64>,
}
impl OneOfWeighted {
pub fn new(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 {
meta: NodeMeta {
name: "one_of_weighted".into(),
outs: vec![Port::str("output")],
ins: vec![Slot::Wire(Port::u64("input"))],
},
values,
cumulative,
}
}
}
impl GkNode for OneOfWeighted {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let h = xxh3_64(&inputs[0].as_u64().to_le_bytes());
let unit = hash_to_unit(h);
let idx = match self.cumulative.binary_search_by(|c| {
c.partial_cmp(&unit).unwrap()
}) {
Ok(i) => i,
Err(i) => i,
};
let idx = idx.min(self.values.len() - 1);
outputs[0] = Value::Str(self.values[idx].clone().into());
}
}
pub struct Blend {
meta: NodeMeta,
mix: f64,
}
impl Blend {
pub fn new(mix: f64) -> Self {
assert!(
(0.0..=1.0).contains(&mix),
"blend: mix must be in [0.0, 1.0], got {mix}"
);
Self {
meta: NodeMeta {
name: "blend".into(),
outs: vec![Port::u64("output")],
ins: vec![
Slot::Wire(Port::u64("a")),
Slot::Wire(Port::u64("b")),
Slot::const_f64("mix", mix),
],
},
mix,
}
}
}
impl GkNode for Blend {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
let a = f64::from_bits(inputs[0].as_u64());
let b = f64::from_bits(inputs[1].as_u64());
let result = a * (1.0 - self.mix) + b * self.mix;
outputs[0] = Value::U64(result.to_bits());
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
let mix = self.mix;
Some(Box::new(move |inputs, outputs| {
let a = f64::from_bits(inputs[0]);
let b = f64::from_bits(inputs[1]);
let result = a * (1.0 - mix) + b * mix;
outputs[0] = result.to_bits();
}))
}
fn jit_constants(&self) -> Vec<u64> { vec![self.mix.to_bits()] }
}
pub struct DefaultOr {
meta: NodeMeta,
}
impl DefaultOr {
pub fn new() -> Self {
Self {
meta: NodeMeta {
name: "default_or".into(),
outs: vec![Port::new("output", PortType::Str)],
ins: vec![
Slot::Wire(Port::new("value", PortType::Str)),
Slot::Wire(Port::new("fallback", PortType::Str)),
],
},
}
}
}
impl GkNode for DefaultOr {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
outputs[0] = if matches!(inputs[0], Value::None) {
inputs[1].clone()
} else {
inputs[0].clone()
};
}
fn accepts_none_inputs(&self) -> bool { true }
}
use crate::dsl::registry::{Arity, FuncCategory, FuncSig, ParamSpec};
use crate::node::SlotType;
pub fn signatures() -> &'static [FuncSig] {
use FuncCategory as C;
&[
FuncSig {
name: "fair_coin", category: C::Probability,
outputs: 1, description: "50/50 binary outcome (0 or 1)",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Fair coin flip: deterministically returns 0 or 1 with 50/50 probability.\nEquivalent to mod(hash(input), 2). Use for simple binary decisions\nlike choosing between two data centers or two code paths.\nParameters:\n input — u64 wire input (hashed internally)\nExample: fair_coin(cycle) // 0 or 1",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "unfair_coin", category: C::Probability,
outputs: 1, description: "biased coin: 1 with probability p, else 0",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "p", slot_type: SlotType::ConstF64, required: true, example: "0.5",
constraint: Some(crate::dsl::const_constraints::ConstConstraint::RangeF64 { min: 0.0, max: 1.0 }) },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Biased coin: returns 1 with probability p, else 0.\nThe input is hashed to [0,1) and compared against p.\nUse for modeling probabilistic events: error injection, cache miss rates.\nParameters:\n input — u64 wire input (hashed internally)\n p — probability of returning 1 (f64 in [0.0, 1.0])\nExample: unfair_coin(cycle, 0.1) // 10% chance of 1",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "select", category: C::Probability,
outputs: 1, description: "binary conditional: if_true when cond != 0, else if_false",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "cond", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "if_true", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "if_false", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Ternary conditional: returns if_true when cond != 0, else if_false.\nAll three inputs are always evaluated (no short-circuit — this is a DAG).\nCombine with fair_coin/unfair_coin/n_of for the condition wire.\nParameters:\n cond — u64 condition (0 = false, nonzero = true)\n if_true — value returned when cond != 0\n if_false — value returned when cond == 0\nExample: select(unfair_coin(cycle, 0.1), slow_path, fast_path)",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "chance", category: C::Probability,
outputs: 1, description: "like unfair_coin but returns f64 (0.0 or 1.0)",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "p", slot_type: SlotType::ConstF64, required: true, example: "0.5",
constraint: Some(crate::dsl::const_constraints::ConstConstraint::RangeF64 { min: 0.0, max: 1.0 }) },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Like unfair_coin but returns 0.0 or 1.0 as f64.\nUse when the result feeds directly into f64 arithmetic\nwithout needing an explicit type conversion step.\nParameters:\n input — u64 wire input (hashed internally)\n p — probability of returning 1.0 (f64 in [0.0, 1.0])\nExample: chance(cycle, 0.3) // 30% chance of 1.0, else 0.0",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "n_of", category: C::Probability,
outputs: 1, description: "exactly n of every m inputs return 1",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "n", slot_type: SlotType::ConstU64, required: true, example: "100", constraint: None },
ParamSpec { name: "m", slot_type: SlotType::ConstU64, required: true, example: "10", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Deterministic fractional selection: exactly n out of every m inputs return 1.\nUnlike unfair_coin (probabilistic), n_of guarantees exact counts\nover each window of m consecutive inputs.\nParameters:\n input — u64 wire input\n n — number of selected inputs per window (u64, n <= m)\n m — window size (u64, must be > 0)\nExample: n_of(cycle, 3, 10) // exactly 3 of every 10 cycles are 1",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "one_of", category: C::Probability,
outputs: 1, description: "uniform selection from N constant values",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "values", slot_type: SlotType::ConstStr, required: true, example: "\"a,b,c\"", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Uniform selection from a comma-separated list of string values.\nHashes the input, picks one value with equal probability.\nUse for simple categorical selection when all outcomes are equally likely.\nParameters:\n input — u64 wire input (hashed internally)\n values — comma-separated string values\nExample: one_of(cycle, \"red,green,blue\")",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "one_of_weighted", category: C::Probability,
outputs: 1, description: "weighted selection from 'val:weight,...' spec",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "input", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "spec", slot_type: SlotType::ConstStr, required: true, example: "\"1:10,2:20,3:30\"",
constraint: Some(crate::dsl::const_constraints::ConstConstraint::StrParser(validate_one_of_weighted_spec)) },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Weighted selection from a \"value:weight,...\" spec string.\nWeights are relative and do not need to sum to 1.\nUse for unequal-probability categorical selection.\nParameters:\n input — u64 wire input (hashed internally)\n spec — comma-separated value:weight pairs\nExample: one_of_weighted(cycle, \"200:80,404:10,500:5,503:5\")",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "blend", category: C::Probability,
outputs: 1, description: "weighted mix: a*(1-mix) + b*mix",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "a", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "b", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "mix", slot_type: SlotType::ConstF64, required: true, example: "0.5",
constraint: Some(crate::dsl::const_constraints::ConstConstraint::RangeF64 { min: 0.0, max: 1.0 }) },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Weighted linear blend of two f64 wire inputs.\nResult = a * (1 - mix) + b * mix. At mix=0 you get pure a, at mix=1 pure b.\nParameters:\n a — first f64 wire input\n b — second f64 wire input\n mix — blend factor (f64 in [0.0, 1.0])\nExample: blend(fast_latency, slow_latency, 0.3) // 70% fast, 30% slow",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
FuncSig {
name: "default_or", category: C::Probability,
outputs: 1, description: "coalesce: return value if set, fallback if None",
identity: None, variadic_ctor: None,
params: &[
ParamSpec { name: "value", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
ParamSpec { name: "fallback", slot_type: SlotType::Wire, required: true, example: "cycle", constraint: None },
],
arity: Arity::Fixed,
commutativity: crate::node::Commutativity::Positional,
help: "Return the first input if it is not None, otherwise the second.\nUse with extern inputs (captures) that may not have been set yet.\nParameters:\n value — primary wire input (may be None if unset)\n fallback — wire input used when value is None\nExample: default_or(username, \"anonymous\")",
default_resolver: None,
output_type: crate::dsl::registry::OutputType::Fixed,
},
]
}
pub(crate) fn build_node(name: &str, _wires: &[crate::assembly::WireRef], _wire_types: &[crate::node::PortType], consts: &[crate::dsl::factory::ConstArg]) -> Option<Result<Box<dyn crate::node::GkNode>, String>> {
match name {
"fair_coin" => Some(Ok(Box::new(FairCoin::new()))),
"unfair_coin" => Some(Ok(Box::new(UnfairCoin::new(
consts.first().map(|c| c.as_f64()).unwrap_or(0.5),
)))),
"select" => Some(Ok(Box::new(Select::new()))),
"chance" => Some(Ok(Box::new(Chance::new(
consts.first().map(|c| c.as_f64()).unwrap_or(0.5),
)))),
"n_of" => Some(Ok(Box::new(NofM::new(
consts.first().map(|c| c.as_u64()).unwrap_or(1),
consts.get(1).map(|c| c.as_u64()).unwrap_or(2),
)))),
"one_of" => {
let values: Vec<String> = consts.iter().map(|c| c.as_str().to_string()).collect();
Some(Ok(Box::new(OneOf::new(values))))
}
"one_of_weighted" => Some(Ok(Box::new(OneOfWeighted::new(
consts.first().map(|c| c.as_str()).unwrap_or("a:1"),
)))),
"blend" => Some(Ok(Box::new(Blend::new(
consts.first().map(|c| c.as_f64()).unwrap_or(0.5),
)))),
"default_or" => Some(Ok(Box::new(DefaultOr::new()))),
_ => None,
}
}
pub(crate) fn validate_node(
name: &str,
consts: &[crate::dsl::factory::ConstArg],
) -> Result<(), String> {
match name {
"n_of" => {
let n = consts.first().map(|c| c.as_u64()).unwrap_or(1);
let m = consts.get(1).map(|c| c.as_u64()).unwrap_or(2);
if m == 0 {
Err("m must be > 0".into())
} else if n > m {
Err(format!("n ({n}) must be <= m ({m})"))
} else {
Ok(())
}
}
"one_of" => {
if consts.is_empty() {
Err("values must be non-empty".into())
} else {
Ok(())
}
}
_ => Ok(()),
}
}
fn validate_one_of_weighted_spec(spec: &str) -> Result<(), String> {
let mut saw_any = false;
let mut total = 0.0f64;
for elem in spec.split([';', ',']) {
let elem = elem.trim();
if elem.is_empty() { continue; }
let parts: Vec<&str> = elem.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(format!("expected 'value:weight', got '{elem}'"));
}
let w: f64 = parts[1].parse()
.map_err(|_| format!("invalid weight '{}'", parts[1]))?;
if w <= 0.0 {
return Err(format!("weight must be positive, got {w}"));
}
saw_any = true;
total += w;
}
if !saw_any {
return Err("spec must be non-empty".into());
}
if total <= 0.0 {
return Err(format!("total weight must be > 0, got {total}"));
}
Ok(())
}
crate::register_nodes!(signatures, build_node, validate_node);
#[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() {
UnfairCoin::new(1.5);
}
#[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 = NofM::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 = NofM::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 = NofM::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 = NofM::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 = NofM::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() {
NofM::new(0, 0);
}
#[test]
#[should_panic(expected = "n_of: n (5) must be <= m (3)")]
fn n_of_m_rejects_n_greater_than_m() {
NofM::new(5, 3);
}
#[test]
fn n_of_m_not_first_n() {
let node = NofM::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() {
OneOf::new(vec![]);
}
#[test]
fn one_of_weighted_selects_from_spec() {
let node = OneOfWeighted::new("red:60,blue:30,green:10");
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");
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");
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");
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");
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("");
}
#[test]
#[should_panic(expected = "one_of_weighted: expected 'value:weight'")]
fn one_of_weighted_rejects_bad_format() {
OneOfWeighted::new("noweight");
}
#[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() {
Blend::new(1.5);
}
#[test]
#[should_panic(expected = "blend: mix must be in [0.0, 1.0]")]
fn blend_rejects_negative_mix() {
Blend::new(-0.1);
}
#[test]
fn default_or_returns_value_when_not_none() {
let node = DefaultOr::new();
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();
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();
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::assembly::{GkAssembler, WireRef};
use crate::nodes::identity::PortPassthrough;
let mut asm = GkAssembler::new(vec!["cycle".into()]);
asm.add_input("captured_name", Value::None, PortType::Str, crate::kernel::InputKind::CapturePort);
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::nodes::identity::ConstStr::new("anonymous".to_string())),
vec![]);
asm.add_node("greeting",
Box::new(DefaultOr::new()),
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);
}
}