use acpu::field::{bit_reverse_permute, multilinear_eval, ntt_forward};
use acpu::{chebyshev_matvec, csr_matvec, csr_matvec_set};
use std::panic::{catch_unwind, AssertUnwindSafe};
fn rejects(message: &str, f: impl FnOnce()) {
let payload = catch_unwind(AssertUnwindSafe(f)).expect_err("input must be rejected");
let text = payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.expect("precondition panic has a message");
assert!(text.contains(message), "expected {message:?}, got {text:?}");
}
type Matvec = fn(&[u32], &[u32], &[f32], &[f32], &mut [f32]);
type CsrCase<'a> = (&'a [u32], &'a [u32], &'a [f32], usize, &'a str);
#[test]
fn csr_rejects_every_malformed_shape_before_output_mutation() {
let cases: &[CsrCase<'_>] = &[
(&[], &[], &[], 0, "row_ptr must be nonempty"),
(&[1, 1], &[0], &[2.0], 1, "row_ptr must start at zero"),
(&[0, 2, 1], &[0], &[2.0], 2, "row_ptr must be nondecreasing"),
(&[0, 0], &[0], &[2.0], 1, "terminal offset"),
(&[0, 2], &[0], &[2.0], 1, "terminal offset"),
(&[0, 1], &[0], &[], 1, "column/value lengths"),
(&[0, 1], &[], &[2.0], 1, "column/value lengths"),
(&[0, 1], &[0], &[2.0], 0, "output length"),
(&[0, 1], &[0], &[2.0], 2, "output length"),
(&[0, 1], &[1], &[2.0], 1, "column index"),
(&[0, 1], &[u32::MAX], &[2.0], 1, "column index"),
(&[0, 1, 2], &[0, 1], &[2.0, 3.0], 2, "column index"),
(
&[0, 1, 2, 3, 4],
&[0, 0, 0, 1],
&[1.0; 4],
4,
"column index",
),
(&[0, 4], &[0, 0, 0, 1], &[1.0; 4], 1, "column index"),
];
for &(row, columns, values, output_len, message) in cases {
for op in [csr_matvec as Matvec, csr_matvec_set] {
let mut y: Vec<f32> = (0..output_len).map(|i| 17.0 + i as f32).collect();
let before = y.clone();
rejects(message, || op(row, columns, values, &[3.0], &mut y));
assert_eq!(y, before, "output changed for {row:?}/{columns:?}");
}
}
for op in [csr_matvec as Matvec, csr_matvec_set] {
let mut y = [17.0];
rejects("column index", || op(&[0, 1], &[0], &[1.0], &[], &mut y));
assert_eq!(y, [17.0]);
}
}
#[test]
fn csr_retains_rectangular_duplicates_empty_rows_and_kernel_paths() {
let row = [0, 0, 1, 18];
let columns = [4, 3, 0, 3, 2, 1, 4, 2, 0, 1, 4, 3, 2, 1, 0, 4, 2, 0];
let values: Vec<f32> = (0..columns.len()).map(|i| (i % 3 + 1) as f32).collect();
let x = [1.0, 2.0, 4.0, 8.0, 16.0];
let expected: Vec<f32> = row
.windows(2)
.map(|pair| {
(pair[0]..pair[1])
.map(|j| values[j] * x[columns[j] as usize])
.sum()
})
.collect();
let mut y = [10.0, 20.0, 30.0];
csr_matvec(&row.map(|x| x as u32), &columns, &values, &x, &mut y);
assert_eq!(
y,
[expected[0] + 10.0, expected[1] + 20.0, expected[2] + 30.0]
);
csr_matvec_set(&row.map(|x| x as u32), &columns, &values, &x, &mut y);
assert_eq!(y.as_slice(), expected);
for op in [csr_matvec as Matvec, csr_matvec_set] {
op(&[0], &[], &[], &[], &mut []);
op(&[0], &[], &[], &x, &mut []); }
let mut empty_rows = [7.0, 9.0];
csr_matvec(&[0, 0, 0], &[], &[], &[], &mut empty_rows);
assert_eq!(empty_rows, [7.0, 9.0]);
csr_matvec_set(&[0, 0, 0], &[], &[], &[], &mut empty_rows);
assert_eq!(empty_rows, [0.0, 0.0]);
}
struct Heat {
row: Vec<u32>,
columns: Vec<u32>,
values: Vec<f32>,
diagonal: Vec<f32>,
x: Vec<f32>,
y: Vec<f32>,
t0: Vec<f32>,
t1: Vec<f32>,
}
impl Heat {
fn valid() -> Self {
Self {
row: vec![0, 1, 2],
columns: vec![1, 0],
values: vec![1.0; 2],
diagonal: vec![1.0; 2],
x: vec![1.0, 0.0],
y: vec![17.0, 19.0],
t0: vec![23.0, 29.0],
t1: vec![31.0, 37.0],
}
}
fn run(&mut self, order: usize) {
chebyshev_matvec(
&self.row,
&self.columns,
&self.values,
&self.diagonal,
&self.x,
&mut self.y,
&mut self.t0,
&mut self.t1,
1.0,
order,
);
}
}
#[test]
fn chebyshev_checks_all_dimensions_and_csr_before_allocation_or_mutation() {
type Change = fn(&mut Heat);
let cases: &[(Change, &str)] = &[
(|h| h.y.clear(), "y length"),
(|h| h.y.push(0.0), "y length"),
(|h| h.t0.clear(), "t0 length"),
(|h| h.t0.push(0.0), "t0 length"),
(|h| h.t1.clear(), "t1 length"),
(|h| h.t1.push(0.0), "t1 length"),
(|h| h.diagonal.clear(), "diagonal length"),
(|h| h.diagonal.push(0.0), "diagonal length"),
(|h| h.x.clear(), "y length"),
(|h| h.row.clear(), "row count"),
(|h| h.row = vec![0, 2], "row count"),
(|h| h.row.push(2), "row count"),
(|h| h.row[0] = 1, "row_ptr must start at zero"),
(|h| h.row = vec![0, 3, 2], "nondecreasing"),
(|h| h.row[2] = 3, "terminal offset"),
(|h| h.values.clear(), "column/value lengths"),
(|h| h.columns[1] = 2, "column index"),
];
for &(change, message) in cases {
for order in [0, 20, usize::MAX] {
let mut h = Heat::valid();
change(&mut h);
let before = (h.y.clone(), h.t0.clone(), h.t1.clone());
rejects(message, || h.run(order));
assert_eq!((h.y, h.t0, h.t1), before);
}
}
}
#[test]
fn chebyshev_reuses_validated_square_matrix_through_recurrence() {
let mut h = Heat::valid();
h.run(20);
let decay = (-2.0_f32).exp();
for (actual, expected) in h.y.iter().zip([(1.0 + decay) / 2.0, (1.0 - decay) / 2.0]) {
assert!((actual - expected).abs() < 1e-6);
}
for order in [0, 1, 2, 20] {
chebyshev_matvec(
&[0],
&[],
&[],
&[],
&[],
&mut [],
&mut [],
&mut [],
1.0,
order,
);
}
}
#[test]
fn field_shape_failures_are_consistent_and_preserve_permutation_inputs() {
for len in [0, 3, 5, 6, 7, 9] {
let mut values: Vec<u64> = (0..len).map(|i| i as u64 + 11).collect();
let before = values.clone();
rejects("bit_reverse_permute: length", || {
bit_reverse_permute(&mut values)
});
assert_eq!(values, before);
rejects("ntt_forward: length", || ntt_forward(&mut values, 1));
assert_eq!(values, before);
rejects("multilinear_eval: length", || {
multilinear_eval(&values, &[0, 0, 0]);
});
}
for len in [1_usize, 2, 4, 8, 16] {
let evals = vec![7; len];
let dimensions = len.trailing_zeros() as usize;
for count in 0..=dimensions + 2 {
if count != dimensions {
rejects("coordinate count", || {
multilinear_eval(&evals, &vec![0; count]);
});
} else {
assert_eq!(multilinear_eval(&evals, &vec![0; count]), 7);
}
}
}
}