mod fusion {
use crate::arithmetic::Mod;
use crate::hash::Hash;
use polydat::ast::PolydatNode;
use polydat::ast::Value;
use polydat::compile::assembly::{PolydatAssembler, WireRef};
use polydat::compile::fusion::{FusedNode, default_rules};
#[test]
fn hash_mod_fuses_to_hash_range() {
let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
asm.add_node("h", Box::new(Hash::new()), vec![WireRef::input("cycle")]);
asm.add_node("m", Box::new(Mod::new(100)), vec![WireRef::node("h")]);
asm.add_output("out", WireRef::node("m"));
let mut kernel = asm.compile().unwrap();
for cycle in 0..1000u64 {
kernel.set_inputs(&[cycle]);
let result = kernel.pull("out").as_u64();
let expected = polydat::numeric::hash::splitmix64_u64(cycle) % 100;
assert_eq!(result, expected, "cycle {cycle}");
}
}
#[test]
fn fusion_skipped_when_intermediate_has_consumers() {
let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
asm.add_node("h", Box::new(Hash::new()), vec![WireRef::input("cycle")]);
asm.add_node("m", Box::new(Mod::new(100)), vec![WireRef::node("h")]);
asm.add_node("m2", Box::new(Mod::new(50)), vec![WireRef::node("h")]);
asm.add_output("out1", WireRef::node("m"));
asm.add_output("out2", WireRef::node("m2"));
let mut kernel = asm.compile().unwrap();
for cycle in 0..100u64 {
kernel.set_inputs(&[cycle]);
let h = polydat::numeric::hash::splitmix64_u64(cycle);
assert_eq!(kernel.pull("out1").as_u64(), h % 100, "out1 cycle {cycle}");
assert_eq!(kernel.pull("out2").as_u64(), h % 50, "out2 cycle {cycle}");
}
}
fn assert_equivalence(fused: &dyn FusedNode, test_count: usize) {
let decomposed = fused.decomposed();
let input_count = fused.meta().wire_inputs().len();
let output_count = fused.meta().outs.len();
for seed in 0..test_count as u64 {
let inputs: Vec<Value> = (0..input_count)
.map(|port| {
let v = xxhash_rust::xxh3::xxh3_64(
&(seed.wrapping_mul(31).wrapping_add(port as u64)).to_le_bytes(),
);
match fused.meta().wire_inputs()[port].typ {
polydat::ast::PortType::U64 => Value::U64(v),
polydat::ast::PortType::F64 => Value::F64(f64::from_bits(v)),
_ => Value::U64(v), }
})
.collect();
let mut fused_outputs = vec![Value::None; output_count];
fused.eval(&inputs, &mut fused_outputs);
let decomposed_outputs = decomposed.eval(&inputs);
for (port_idx, (fused_val, decomposed_val)) in fused_outputs
.iter()
.zip(decomposed_outputs.iter())
.enumerate()
{
match (&fused_val, &decomposed_val) {
(Value::U64(a), Value::U64(b)) => {
assert_eq!(
a, b,
"equivalence failed: seed={seed} port={port_idx} fused={a} decomposed={b}"
);
}
(Value::F64(a), Value::F64(b)) => {
let diff = (a - b).abs();
let tolerance = 1e-10 * a.abs().max(b.abs()).max(1.0);
assert!(
diff <= tolerance,
"equivalence failed: seed={seed} port={port_idx} fused={a} decomposed={b} diff={diff}"
);
}
_ => {
assert_eq!(
fused_val.to_display_string(),
decomposed_val.to_display_string(),
"equivalence failed: seed={seed} port={port_idx}"
);
}
}
}
}
}
#[test]
fn hash_range_equivalence() {
use crate::hash::HashRange;
for max in [1, 2, 7, 100, 10_000, u64::MAX] {
let fused = HashRange::new(max);
assert_equivalence(&fused, 10_000);
}
}
#[test]
fn hash_interval_equivalence() {
use crate::hash::HashInterval;
for (lo, hi) in [(0.0, 1.0), (-180.0, 180.0), (0.0, 1000.0), (-1.0, -0.5)] {
let fused = HashInterval::new(lo, hi);
assert_equivalence(&fused, 10_000);
}
}
#[test]
fn scale_range_equivalence() {
use crate::lerp::ScaleRange;
for (lo, hi) in [(0.0, 1.0), (-100.0, 100.0), (0.0, 360.0), (1e6, 1e7)] {
let fused = ScaleRange::new(lo, hi);
assert_equivalence(&fused, 10_000);
}
}
#[test]
fn all_default_rules_produce_equivalent_nodes() {
let rules = default_rules();
for rule in &rules {
assert!(
rule.pattern.root_op().is_some(),
"rule '{}' has no root op",
rule.name
);
assert!(
!rule.input_bindings.is_empty(),
"rule '{}' has no input bindings",
rule.name
);
}
}
#[test]
fn variadic_pattern_matches_sum() {
use crate::arithmetic::Sum;
let mut asm = PolydatAssembler::new(vec!["a".into(), "b".into(), "c".into()]);
asm.add_node(
"s",
Box::new(Sum::new(3)),
vec![
WireRef::input("a"),
WireRef::input("b"),
WireRef::input("c"),
],
);
asm.add_output("out", WireRef::node("s"));
let mut kernel = asm.compile().unwrap();
kernel.set_inputs(&[10, 20, 30]);
assert_eq!(kernel.pull("out").as_u64(), 60);
}
#[test]
fn typed_constants_captured_in_match() {
use crate::hash::HashRange;
use polydat::ast::ConstValue;
let mut asm = PolydatAssembler::new(vec!["cycle".into()]);
asm.add_node(
"hr",
Box::new(HashRange::new(100)),
vec![WireRef::input("cycle")],
);
asm.add_output("out", WireRef::node("hr"));
let node = HashRange::new(100);
let typed: Vec<ConstValue> = node
.meta()
.const_slots()
.iter()
.map(|c| c.1.clone())
.collect();
assert_eq!(typed.len(), 1);
assert_eq!(typed[0], ConstValue::U64(100));
}
}
mod simd_plan {
use polydat::ast::PortType;
use polydat::compile::simd_plan::{SimdVariantError, validate_simd_variant};
#[test]
fn exact_core_variants_validate_by_signature() {
let add = crate::bitwise::U64Add::new();
let add_variant = validate_simd_variant(&add).unwrap();
assert_eq!(add_variant.vector_node, "reg_add_i64");
assert_eq!(add_variant.shape.scalar, PortType::U64);
assert_eq!(add_variant.shape.lanes, 2);
let mul = crate::math::F64Mul::new();
let mul_variant = validate_simd_variant(&mul).unwrap();
assert_eq!(mul_variant.vector_node, "reg_mul_f64");
assert_eq!(mul_variant.shape.register, PortType::RegF64x2);
}
#[test]
fn undeclared_nodes_remain_ineligible() {
let node = crate::bitwise::U64Div::new();
assert_eq!(
validate_simd_variant(&node),
Err(SimdVariantError::Undeclared)
);
}
}
#[cfg(feature = "jit")]
mod codegen {
use polydat::compile::jit::{JitOp, classify_node, compile_jit_raw};
use std::collections::HashMap;
#[test]
fn jit_shuffle() {
use crate::sampling::metashift::{Shuffle, feedback_for_size};
use polydat::ast::PolydatNode;
let size = 1000u64;
let node = Shuffle::new(feedback_for_size(size), size, 0);
let consts = node.jit_constants();
let steps = vec![(
JitOp::ShuffleConst(consts[0], consts[1], consts[2]),
vec![0],
vec![1],
)];
let mut output_map = HashMap::new();
output_map.insert("out".into(), 1);
let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
kernel.eval(&[42]);
let jit_result = kernel.get("out");
let mut out = [polydat::ast::Value::None];
node.eval(&[polydat::ast::Value::U64(42)], &mut out);
assert_eq!(jit_result, out[0].as_u64());
}
#[test]
fn jit_lut_sample() {
use crate::sampling::lut::LutF64;
let lut = LutF64::from_fn(|p| p * 100.0, 1000);
let lut_ptr = lut.as_ptr() as u64;
let lut_len = lut.len() as u64;
let steps = vec![(JitOp::LutSampleConst(lut_ptr, lut_len), vec![0], vec![1])];
let mut output_map = HashMap::new();
output_map.insert("out".into(), 1);
let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
kernel.eval(&[0.5f64.to_bits()]);
let v = f64::from_bits(kernel.get("out"));
assert!((v - 50.0).abs() < 0.1, "got {v}");
kernel.eval(&[0.0f64.to_bits()]);
let v = f64::from_bits(kernel.get("out"));
assert!((v - 0.0).abs() < 0.1, "got {v}");
kernel.eval(&[1.0f64.to_bits()]);
let v = f64::from_bits(kernel.get("out"));
assert!((v - 100.0).abs() < 0.1, "got {v}");
}
#[test]
fn jit_lut_normal_distribution() {
use crate::sampling::icd;
let lut = icd::dist_normal_lut(0.0, 1.0, icd::DEFAULT_RESOLUTION);
let lut_ptr = lut.as_ptr() as u64;
let lut_len = lut.len() as u64;
let steps = vec![(JitOp::LutSampleConst(lut_ptr, lut_len), vec![0], vec![1])];
let mut output_map = HashMap::new();
output_map.insert("out".into(), 1);
let mut kernel = compile_jit_raw(1, 2, steps, output_map, Vec::new()).unwrap();
kernel.eval(&[0.5f64.to_bits()]);
let v = f64::from_bits(kernel.get("out"));
assert!((v - 0.0).abs() < 0.01, "median should be ~0, got {v}");
kernel.eval(&[0.8413f64.to_bits()]);
let v = f64::from_bits(kernel.get("out"));
assert!((v - 1.0).abs() < 0.05, "1σ should be ~1.0, got {v}");
}
#[test]
fn classify_routes_new_param_helpers() {
use crate::param_helpers::{InRange, IsPositive};
let p = IsPositive::new("rate".to_string());
assert!(matches!(classify_node(&p), JitOp::IsPositiveCheck { .. }));
let r = InRange::new(1, 100);
assert!(matches!(classify_node(&r), JitOp::InRangeCheck(1, 100)));
}
#[test]
fn classify_leaves_other_param_helpers_on_fallback() {
use crate::param_helpers::{Matches, Required, ThisOr};
assert!(matches!(
classify_node(&Required::new("x".to_string())),
JitOp::Fallback
));
assert!(matches!(classify_node(&ThisOr::new()), JitOp::Fallback));
assert!(matches!(
classify_node(&Matches::new(r"^\d+$".to_string())),
JitOp::Fallback
));
}
#[test]
fn classify_routes_is_one_of_to_fallback() {
use crate::param_helpers::IsOneOf;
let n = IsOneOf::new(vec![1, 3, 5, 7]);
assert!(matches!(classify_node(&n), JitOp::Fallback));
}
}