1use std::fmt::Debug;
25
26use itertools::Itertools;
27use num_traits::Bounded;
28use num_traits::CheckedAdd;
29use num_traits::CheckedDiv;
30use num_traits::CheckedMul;
31use num_traits::CheckedSub;
32use num_traits::Float;
33use num_traits::Num;
34use num_traits::Signed;
35use vortex_error::VortexExpect;
36use vortex_error::vortex_panic;
37
38use crate::ArrayRef;
39use crate::Canonical;
40use crate::ExecutionCtx;
41use crate::IntoArray;
42use crate::RecursiveCanonical;
43use crate::arrays::ConstantArray;
44use crate::builtins::ArrayBuiltins;
45use crate::dtype::DType;
46use crate::dtype::DecimalDType;
47use crate::dtype::NativeDecimalType;
48use crate::dtype::NativePType;
49use crate::dtype::PType;
50use crate::dtype::i256;
51use crate::scalar::DecimalValue;
52use crate::scalar::NumericOperator;
53use crate::scalar::PrimitiveScalar;
54use crate::scalar::Scalar;
55use crate::scalar_fn::fns::binary::numeric_op_result_decimal_dtype;
56
57fn to_vec_of_scalar(array: &ArrayRef, ctx: &mut ExecutionCtx) -> Vec<Scalar> {
58 (0..array.len())
60 .map(|index| {
61 array
62 .execute_scalar(index, ctx)
63 .vortex_expect("scalar_at should succeed in conformance test")
64 })
65 .collect_vec()
66}
67
68fn test_binary_numeric_conformance<T: NativePType + Num + Copy>(
93 array: &ArrayRef,
94 ctx: &mut ExecutionCtx,
95) where
96 Scalar: From<T>,
97{
98 test_binary_numeric_with_scalar(array, T::one(), &[true, false], ctx);
100
101 test_binary_numeric_edge_cases(array, ctx);
103}
104
105pub fn test_binary_numeric_array(array: &ArrayRef, ctx: &mut ExecutionCtx) {
122 match array.dtype() {
123 DType::Primitive(ptype, _) => match ptype {
124 PType::I8 => test_binary_numeric_conformance::<i8>(array, ctx),
125 PType::I16 => test_binary_numeric_conformance::<i16>(array, ctx),
126 PType::I32 => test_binary_numeric_conformance::<i32>(array, ctx),
127 PType::I64 => test_binary_numeric_conformance::<i64>(array, ctx),
128 PType::U8 => test_binary_numeric_conformance::<u8>(array, ctx),
129 PType::U16 => test_binary_numeric_conformance::<u16>(array, ctx),
130 PType::U32 => test_binary_numeric_conformance::<u32>(array, ctx),
131 PType::U64 => test_binary_numeric_conformance::<u64>(array, ctx),
132 PType::F16 => {
133 eprintln!("Skipping f16 binary numeric tests (not supported)");
135 }
136 PType::F32 => test_binary_numeric_conformance::<f32>(array, ctx),
137 PType::F64 => test_binary_numeric_conformance::<f64>(array, ctx),
138 },
139 DType::Decimal(decimal_dtype, _) => {
140 test_binary_numeric_conformance_decimal(array, *decimal_dtype, ctx)
141 }
142 dtype => vortex_panic!(
143 "Binary numeric tests are only supported for primitive and decimal types, got {dtype}",
144 ),
145 }
146}
147
148fn test_binary_numeric_conformance_decimal(
152 array: &ArrayRef,
153 decimal_dtype: DecimalDType,
154 ctx: &mut ExecutionCtx,
155) {
156 let precision = decimal_dtype.precision() as usize;
157 let prec_max = <i256 as NativeDecimalType>::MAX_BY_PRECISION[precision];
158
159 let mut constants = vec![
160 i256::from_i128(0),
161 i256::from_i128(1),
162 i256::from_i128(-1),
163 prec_max,
164 ];
165 if decimal_dtype.scale() >= 0
167 && let Some(one) = i256::from_i128(10).checked_pow(decimal_dtype.scale() as u32)
168 && one <= prec_max
169 {
170 constants.push(one);
171 }
172
173 for constant in constants {
174 let value = DecimalValue::try_from_i256(constant, decimal_dtype)
175 .vortex_expect("conformance constants fit the precision");
176 test_decimal_binary_numeric_with_scalar(array, value, decimal_dtype, ctx);
177 }
178}
179
180fn test_decimal_binary_numeric_with_scalar(
181 array: &ArrayRef,
182 value: DecimalValue,
183 decimal_dtype: DecimalDType,
184 ctx: &mut ExecutionCtx,
185) {
186 let canonicalized_array = array
187 .clone()
188 .execute::<Canonical>(ctx)
189 .vortex_expect("Must be able to canonicalise")
190 .into_array();
191 let original_values = to_vec_of_scalar(&canonicalized_array, ctx);
192
193 let scalar = Scalar::decimal(value, decimal_dtype, array.dtype().nullability());
194
195 let mut operators = vec![
196 NumericOperator::Add,
197 NumericOperator::Sub,
198 NumericOperator::Mul,
199 ];
200 if !value.is_zero() {
201 operators.push(NumericOperator::Div);
202 }
203
204 for operator in operators {
205 for lhs_is_array in [true, false] {
206 test_decimal_binary_numeric_direction(
207 array,
208 &original_values,
209 &scalar,
210 decimal_dtype,
211 operator,
212 lhs_is_array,
213 ctx,
214 );
215 }
216 }
217}
218
219fn test_decimal_binary_numeric_direction(
220 array: &ArrayRef,
221 original_values: &[Scalar],
222 scalar: &Scalar,
223 decimal_dtype: DecimalDType,
224 operator: NumericOperator,
225 lhs_is_array: bool,
226 ctx: &mut ExecutionCtx,
227) {
228 let result_decimal_dtype = numeric_op_result_decimal_dtype(decimal_dtype, operator)
229 .vortex_expect("decimal arithmetic must have a result dtype");
230 let result_dtype = DType::Decimal(result_decimal_dtype, array.dtype().nullability());
231 let expected_results = expected_decimal_results(
232 original_values,
233 scalar,
234 operator,
235 result_decimal_dtype,
236 &result_dtype,
237 lhs_is_array,
238 );
239
240 let constant = ConstantArray::new(scalar.clone(), array.len()).into_array();
241 let (lhs, rhs) = if lhs_is_array {
242 (array.clone(), constant)
243 } else {
244 (constant, array.clone())
245 };
246 let result = lhs
247 .binary(rhs, operator.into())
248 .vortex_expect("binary decimal op shouldn't fail")
249 .execute::<RecursiveCanonical>(ctx)
250 .map(|c| c.0.into_array());
251
252 assert_decimal_results(
253 result,
254 &expected_results,
255 array,
256 scalar,
257 operator,
258 lhs_is_array,
259 ctx,
260 );
261}
262
263fn expected_decimal_results(
264 original_values: &[Scalar],
265 scalar: &Scalar,
266 operator: NumericOperator,
267 result_decimal_dtype: DecimalDType,
268 result_dtype: &DType,
269 lhs_is_array: bool,
270) -> Vec<Option<Scalar>> {
271 original_values
272 .iter()
273 .map(|value| {
274 let (lhs, rhs) = if lhs_is_array {
275 (value.as_decimal(), scalar.as_decimal())
276 } else {
277 (scalar.as_decimal(), value.as_decimal())
278 };
279 let (Some(lhs), Some(rhs)) = (lhs.decimal_value(), rhs.decimal_value()) else {
280 return Some(Scalar::null(result_dtype.clone()));
281 };
282 let lhs = lhs.as_i256();
283 let rhs = rhs.as_i256();
284 let value = match operator {
285 NumericOperator::Add => lhs.checked_add(&rhs),
286 NumericOperator::Sub => lhs.checked_sub(&rhs),
287 NumericOperator::Mul => lhs.checked_mul(&rhs),
288 NumericOperator::Div => {
289 let scale_power = result_decimal_dtype.scale();
290 let factor =
291 i256::from_i128(10).checked_pow(scale_power.unsigned_abs() as u32)?;
292 if scale_power >= 0 {
293 lhs.checked_mul(&factor)?.checked_div(&rhs)
294 } else {
295 lhs.checked_div(&rhs.checked_mul(&factor)?)
296 }
297 }
298 }?;
299 let value = DecimalValue::try_from_i256(value, result_decimal_dtype).ok()?;
300 Some(Scalar::decimal(
301 value,
302 result_decimal_dtype,
303 result_dtype.nullability(),
304 ))
305 })
306 .collect()
307}
308
309fn assert_decimal_results(
310 result: vortex_error::VortexResult<ArrayRef>,
311 expected_results: &[Option<Scalar>],
312 array: &ArrayRef,
313 scalar: &Scalar,
314 operator: NumericOperator,
315 lhs_is_array: bool,
316 ctx: &mut ExecutionCtx,
317) {
318 if expected_results.iter().any(Option::is_none) {
319 assert!(
320 result.is_err(),
321 "Decimal binary numeric operation should overflow for encoding {}: \
322 {operator:?} {scalar} (lhs_is_array: {lhs_is_array})",
323 array.encoding_id(),
324 );
325 return;
326 }
327
328 let result = result.unwrap_or_else(|err| {
329 vortex_panic!(
330 "Decimal binary numeric operation unexpectedly failed for encoding {}: \
331 {operator:?} {scalar} (lhs_is_array: {lhs_is_array}): {err}",
332 array.encoding_id(),
333 )
334 });
335
336 assert_eq!(result.len(), expected_results.len());
337 let actual_values = to_vec_of_scalar(&result, ctx);
338 for (idx, (actual, expected)) in actual_values.iter().zip(expected_results).enumerate() {
339 let expected_value = expected
340 .as_ref()
341 .vortex_expect("non-overflowing decimal lane must have an expected value");
342 assert_eq!(
343 actual,
344 expected_value,
345 "Decimal binary numeric operation failed for encoding {} at index {}: \
346 ({array:?})[{idx}] {operator:?} {scalar} (lhs_is_array: {lhs_is_array}) \
347 expected {expected_value:?}, got {actual:?}",
348 array.encoding_id(),
349 idx,
350 );
351 }
352}
353
354fn test_binary_numeric_edge_cases(array: &ArrayRef, ctx: &mut ExecutionCtx) {
362 match array.dtype() {
363 DType::Primitive(ptype, _) => match ptype {
364 PType::I8 => test_binary_numeric_edge_cases_signed::<i8>(array, ctx),
365 PType::I16 => test_binary_numeric_edge_cases_signed::<i16>(array, ctx),
366 PType::I32 => test_binary_numeric_edge_cases_signed::<i32>(array, ctx),
367 PType::I64 => test_binary_numeric_edge_cases_signed::<i64>(array, ctx),
368 PType::U8 => test_binary_numeric_edge_cases_unsigned::<u8>(array, ctx),
369 PType::U16 => test_binary_numeric_edge_cases_unsigned::<u16>(array, ctx),
370 PType::U32 => test_binary_numeric_edge_cases_unsigned::<u32>(array, ctx),
371 PType::U64 => test_binary_numeric_edge_cases_unsigned::<u64>(array, ctx),
372 PType::F16 => {
373 eprintln!("Skipping f16 edge case tests (not supported)");
374 }
375 PType::F32 => test_binary_numeric_edge_cases_float::<f32>(array, ctx),
376 PType::F64 => test_binary_numeric_edge_cases_float::<f64>(array, ctx),
377 },
378 dtype => vortex_panic!(
379 "Binary numeric edge case tests are only supported for primitive numeric types, got {dtype}"
380 ),
381 }
382}
383
384fn test_binary_numeric_edge_cases_signed<T>(array: &ArrayRef, ctx: &mut ExecutionCtx)
385where
386 T: NativePType + Num + Copy + Debug + Bounded + Signed,
387 Scalar: From<T>,
388{
389 test_binary_numeric_with_scalar(array, T::zero(), &[true], ctx);
391
392 test_binary_numeric_with_scalar(array, -T::one(), &[true], ctx);
394
395 test_binary_numeric_with_scalar(array, T::max_value(), &[true], ctx);
397
398 test_binary_numeric_with_scalar(array, T::min_value(), &[true], ctx);
400}
401
402fn test_binary_numeric_edge_cases_unsigned<T>(array: &ArrayRef, ctx: &mut ExecutionCtx)
403where
404 T: NativePType + Num + Copy + Debug + Bounded,
405 Scalar: From<T>,
406{
407 test_binary_numeric_with_scalar(array, T::zero(), &[true], ctx);
409
410 test_binary_numeric_with_scalar(array, T::max_value(), &[true], ctx);
412}
413
414fn test_binary_numeric_edge_cases_float<T>(array: &ArrayRef, ctx: &mut ExecutionCtx)
415where
416 T: NativePType + Num + Copy + Debug + Float,
417 Scalar: From<T>,
418{
419 test_binary_numeric_with_scalar(array, T::zero(), &[true], ctx);
421
422 test_binary_numeric_with_scalar(array, -T::one(), &[true], ctx);
424
425 test_binary_numeric_with_scalar(array, T::max_value(), &[true], ctx);
427
428 test_binary_numeric_with_scalar(array, T::min_value(), &[true], ctx);
430
431 test_binary_numeric_with_scalar(array, T::epsilon(), &[true], ctx);
433
434 test_binary_numeric_with_scalar(array, T::min_positive_value(), &[true], ctx);
436
437 test_binary_numeric_with_scalar(array, T::nan(), &[true], ctx);
439 test_binary_numeric_with_scalar(array, T::infinity(), &[true], ctx);
440 test_binary_numeric_with_scalar(array, T::neg_infinity(), &[true], ctx);
441}
442
443fn test_binary_numeric_with_scalar<T>(
444 array: &ArrayRef,
445 scalar_value: T,
446 directions: &[bool],
447 ctx: &mut ExecutionCtx,
448) where
449 T: NativePType + Num + Copy + Debug,
450 Scalar: From<T>,
451{
452 let canonicalized_array = array
453 .clone()
454 .execute::<Canonical>(ctx)
455 .vortex_expect("Must be able to canonicalise")
456 .into_array();
457 let original_values = to_vec_of_scalar(&canonicalized_array, ctx);
458 let scalar = Scalar::from(scalar_value)
459 .cast(array.dtype())
460 .vortex_expect("constant must fit the array dtype");
461
462 for operator in [
463 NumericOperator::Add,
464 NumericOperator::Sub,
465 NumericOperator::Mul,
466 NumericOperator::Div,
467 ] {
468 if operator == NumericOperator::Div && scalar_value == T::zero() {
469 continue;
470 }
471 for &lhs_is_array in directions {
472 let expected_results: Vec<Option<Scalar>> = original_values
473 .iter()
474 .map(|value| {
475 let (lhs, rhs) = if lhs_is_array {
476 (value.as_primitive(), scalar.as_primitive())
477 } else {
478 (scalar.as_primitive(), value.as_primitive())
479 };
480 lhs.checked_binary_numeric(&rhs, operator)
481 .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
482 })
483 .collect();
484 let constant = ConstantArray::new(scalar.clone(), array.len()).into_array();
485 let (lhs, rhs) = if lhs_is_array {
486 (array.clone(), constant)
487 } else {
488 (constant, array.clone())
489 };
490 let result = lhs
491 .binary(rhs, operator.into())
492 .vortex_expect("binary numeric construction must succeed")
493 .execute::<RecursiveCanonical>(ctx);
494 let result = match result {
495 Ok(result) => result.0.into_array(),
496 Err(err) => {
497 assert!(
498 expected_results.iter().any(Option::is_none),
499 "Binary numeric operation unexpectedly failed for encoding {}: \
500 {operator:?} {scalar} (lhs_is_array: {lhs_is_array}): {err}",
501 array.encoding_id(),
502 );
503 continue;
504 }
505 };
506 assert_eq!(result.len(), expected_results.len());
507 assert_eq!(result.dtype(), array.dtype());
508 let actual_values = to_vec_of_scalar(&result, ctx);
509 for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate()
510 {
511 if let Some(expected) = expected {
512 assert_eq!(
513 actual,
514 expected,
515 "Binary numeric operation failed for encoding {} at index {idx}: \
516 {operator:?} {scalar} (lhs_is_array: {lhs_is_array})",
517 array.encoding_id(),
518 );
519 }
520 }
521 }
522 }
523}