use crate::derive_support::Ext;
use crate::iteration::cursor_partition::{Partition, PartitionList};
#[crate::polydat_node(category = Arithmetic)]
fn cardinality(partition: Ext<Partition>) -> u64 {
partition.cardinality()
}
#[crate::polydat_node(category = Arithmetic)]
fn start_of(partition: Ext<Partition>) -> u64 {
partition.start_ord
}
#[crate::polydat_node(category = Arithmetic)]
fn end_of(partition: Ext<Partition>) -> u64 {
partition.end_ord
}
#[crate::polydat_node(category = Arithmetic)]
fn idx_of(partition: Ext<Partition>) -> u64 {
partition.idx
}
#[crate::polydat_node(category = Arithmetic)]
fn count_of(partition: Ext<Partition>) -> u64 {
partition.count
}
#[crate::polydat_node(category = Arithmetic)]
fn mod_in(n: u64, partition: Ext<Partition>) -> u64 {
let card = partition.cardinality();
if card == 0 { partition.start_ord } else { partition.start_ord + (n % card) }
}
#[crate::polydat_node(category = Arithmetic)]
fn at(partition: Ext<Partition>, i: u64) -> u64 {
let card = partition.cardinality();
if i >= card {
panic!(
"at({}, {i}): index out of range — partition #{} cardinality is {card}",
partition.start_ord, partition.idx
);
}
partition.start_ord + i
}
#[crate::polydat_node(category = Arithmetic)]
fn clamp_in(n: u64, partition: Ext<Partition>) -> u64 {
if partition.cardinality() == 0 {
partition.start_ord
} else {
n.max(partition.start_ord).min(partition.end_ord - 1)
}
}
#[crate::polydat_node(category = Hashing)]
fn random_in(partition: Ext<Partition>, seed: u64) -> u64 {
let card = partition.cardinality();
if card == 0 {
partition.start_ord
} else {
partition.start_ord + crate::library::hash::splitmix64_u64(seed) % card
}
}
#[crate::polydat_node(category = Arithmetic)]
fn subdivide(partition: Ext<Partition>, n: u64) -> Ext<PartitionList> {
let parts = crate::iteration::cursor_partition::subdivide_partition(&partition, n)
.unwrap_or_else(|e| panic!("{e}"));
Ext(PartitionList(std::sync::Arc::new(parts)))
}
#[crate::polydat_node(category = Arithmetic)]
fn partitions(
spec: &str,
#[poly_default(100u64)] extent: crate::derive_support::Const<u64>,
) -> Ext<PartitionList> {
let parsed = crate::iteration::cursor_partition::parse(spec)
.unwrap_or_else(|e| panic!("partitions: bad spec `{spec}`: {e}"));
let parts = crate::iteration::cursor_partition::resolve(&parsed, 0, *extent)
.unwrap_or_else(|e| panic!("partitions: resolve failed: {e}"));
Ext(PartitionList(std::sync::Arc::new(parts)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PolydatNode, Value};
fn fixture(idx: u64, start: u64, end: u64) -> Partition {
Partition {
idx,
count: idx + 1,
start_ord: start,
end_ord: end,
start_pct: 0.0,
end_pct: 0.0,
base_extent: end,
}
}
#[test]
fn cardinality_returns_end_minus_start() {
let node = Cardinality::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(0, 100, 500))], &mut out);
assert_eq!(out[0].as_u64(), 400);
}
#[test]
fn start_of_returns_start_ord() {
let node = StartOf::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(2, 100, 500))], &mut out);
assert_eq!(out[0].as_u64(), 100);
}
#[test]
fn end_of_returns_end_ord() {
let node = EndOf::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(0, 100, 500))], &mut out);
assert_eq!(out[0].as_u64(), 500);
}
#[test]
fn idx_of_returns_idx() {
let node = IdxOf::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(3, 100, 500))], &mut out);
assert_eq!(out[0].as_u64(), 3);
}
#[test]
fn mod_in_wraps_inside_partition() {
let node = ModIn::new();
let mut out = [Value::None];
let p = Value::from_partition(fixture(0, 100, 200));
for (n, expected) in [(0, 100), (50, 150), (99, 199), (100, 100), (250, 150)] {
node.eval(&[Value::U64(n), p.clone()], &mut out);
assert_eq!(out[0].as_u64(), expected, "mod_in({n}) over [100, 200)");
}
}
#[test]
fn mod_in_zero_cardinality_returns_start() {
let node = ModIn::new();
let mut out = [Value::None];
let p = Value::from_partition(fixture(0, 100, 100));
node.eval(&[Value::U64(42), p], &mut out);
assert_eq!(out[0].as_u64(), 100);
}
#[test]
fn at_offset_within_bounds() {
let node = At::new();
let mut out = [Value::None];
let p = Value::from_partition(fixture(0, 100, 200));
node.eval(&[p, Value::U64(15)], &mut out);
assert_eq!(out[0].as_u64(), 115);
}
#[test]
#[should_panic(expected = "index out of range")]
fn at_offset_out_of_range_panics() {
let node = At::new();
let mut out = [Value::None];
let p = Value::from_partition(fixture(0, 100, 200));
node.eval(&[p, Value::U64(100)], &mut out);
}
#[test]
fn clamp_in_saturates_at_bounds() {
let node = ClampIn::new();
let mut out = [Value::None];
let p = Value::from_partition(fixture(0, 100, 200));
for (n, expected) in [(50, 100), (100, 100), (150, 150), (199, 199), (200, 199), (1000, 199)] {
node.eval(&[Value::U64(n), p.clone()], &mut out);
assert_eq!(out[0].as_u64(), expected, "clamp_in({n}) over [100, 200)");
}
}
#[test]
fn random_in_deterministic_and_bounded() {
let node = RandomIn::new();
let mut out = [Value::None];
let p = Value::from_partition(fixture(0, 100, 200));
let mut first = Vec::new();
for seed in 0..32u64 {
node.eval(&[p.clone(), Value::U64(seed)], &mut out);
let v = out[0].as_u64();
assert!((100..200).contains(&v), "random_in(seed={seed}) = {v} outside [100, 200)");
first.push(v);
}
for (seed, expected) in first.iter().enumerate() {
node.eval(&[p.clone(), Value::U64(seed as u64)], &mut out);
assert_eq!(out[0].as_u64(), *expected);
}
assert!(first.windows(2).any(|w| w[0] != w[1]));
}
#[test]
fn random_in_zero_cardinality_returns_start() {
let node = RandomIn::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(0, 100, 100)), Value::U64(7)], &mut out);
assert_eq!(out[0].as_u64(), 100);
}
#[test]
fn subdivide_splits_into_near_equal_contiguous_parts() {
let node = Subdivide::new();
let mut out = [Value::None];
let parent = Partition {
idx: 1,
count: 2,
start_ord: 900,
end_ord: 1000,
start_pct: 90.0,
end_pct: 100.0,
base_extent: 1000,
};
node.eval(&[Value::from_partition(parent), Value::U64(10)], &mut out);
let list = out[0].as_partition_list().expect("PartitionList");
assert_eq!(list.len(), 10);
let subs = list.as_slice();
assert_eq!(subs[0].start_ord, 900);
assert_eq!(subs[9].end_ord, 1000);
for (i, s) in subs.iter().enumerate() {
assert_eq!(s.idx, i as u64, "indices restart at 0");
assert_eq!(s.cardinality(), 10);
assert_eq!(s.base_extent, 1000, "base_extent propagates");
}
for w in subs.windows(2) {
assert_eq!(w[0].end_ord, w[1].start_ord, "contiguous");
}
assert!((subs[0].start_pct - 90.0).abs() < 1e-9);
assert!((subs[4].end_pct - 95.0).abs() < 1e-9);
assert!((subs[9].end_pct - 100.0).abs() < 1e-9);
}
#[test]
#[should_panic(expected = "non-empty sub-partitions")]
fn subdivide_finer_than_cardinality_panics() {
let node = Subdivide::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(0, 0, 5)), Value::U64(10)], &mut out);
}
#[test]
#[should_panic(expected = "must be >= 1")]
fn subdivide_zero_count_panics() {
let node = Subdivide::new();
let mut out = [Value::None];
node.eval(&[Value::from_partition(fixture(0, 0, 100)), Value::U64(0)], &mut out);
}
#[test]
fn partitions_node_resolves_spec_against_extent() {
let node = Partitions::new(1000);
let mut out = [Value::None];
node.eval(&[Value::Str("linear:4".into())], &mut out);
let list = out[0].as_partition_list().expect("PartitionList");
assert_eq!(list.len(), 4);
for (i, p) in list.as_slice().iter().enumerate() {
assert_eq!(p.idx, i as u64);
assert_eq!(p.cardinality(), 250);
}
}
#[test]
fn partitions_node_handles_form1_single_range() {
let node = Partitions::new(1000);
let mut out = [Value::None];
node.eval(&[Value::Str("0..50%".into())], &mut out);
let list = out[0].as_partition_list().expect("PartitionList");
assert_eq!(list.len(), 1);
assert_eq!(list.as_slice()[0].start_ord, 0);
assert_eq!(list.as_slice()[0].end_ord, 500);
}
}