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_err;
37use vortex_error::vortex_panic;
38
39use crate::ArrayRef;
40use crate::Canonical;
41use crate::ExecutionCtx;
42use crate::IntoArray;
43use crate::RecursiveCanonical;
44use crate::arrays::ConstantArray;
45use crate::builtins::ArrayBuiltins;
46use crate::dtype::DType;
47use crate::dtype::DecimalDType;
48use crate::dtype::NativeDecimalType;
49use crate::dtype::NativePType;
50use crate::dtype::PType;
51use crate::dtype::i256;
52use crate::scalar::DecimalValue;
53use crate::scalar::NumericOperator;
54use crate::scalar::PrimitiveScalar;
55use crate::scalar::Scalar;
56use crate::scalar_fn::fns::binary::numeric_op_result_decimal_dtype;
57
58fn to_vec_of_scalar(array: &ArrayRef, ctx: &mut ExecutionCtx) -> Vec<Scalar> {
59 (0..array.len())
61 .map(|index| {
62 array
63 .execute_scalar(index, ctx)
64 .vortex_expect("scalar_at should succeed in conformance test")
65 })
66 .collect_vec()
67}
68
69fn test_binary_numeric_conformance<T: NativePType + Num + Copy>(
94 array: &ArrayRef,
95 ctx: &mut ExecutionCtx,
96) where
97 Scalar: From<T>,
98{
99 test_standard_binary_numeric::<T>(array, ctx);
101
102 test_binary_numeric_edge_cases(array, ctx);
104}
105
106fn test_standard_binary_numeric<T: NativePType + Num + Copy>(
107 array: &ArrayRef,
108 ctx: &mut ExecutionCtx,
109) where
110 Scalar: From<T>,
111{
112 let canonicalized_array = array
113 .clone()
114 .execute::<Canonical>(ctx)
115 .vortex_expect("Must be able to canonicalise")
116 .into_array();
117 let original_values = to_vec_of_scalar(&canonicalized_array, ctx);
118
119 let one = T::from(1)
120 .ok_or_else(|| vortex_err!("could not convert 1 into array native type"))
121 .vortex_expect("operation should succeed in conformance test");
122 let scalar_one = Scalar::from(one)
123 .cast(array.dtype())
124 .vortex_expect("operation should succeed in conformance test");
125
126 let operators: [NumericOperator; 4] = [
127 NumericOperator::Add,
128 NumericOperator::Sub,
129 NumericOperator::Mul,
130 NumericOperator::Div,
131 ];
132
133 for operator in operators {
134 let op = operator;
135 let rhs_const = ConstantArray::new(scalar_one.clone(), array.len()).into_array();
136
137 let result = array
139 .binary(rhs_const.clone(), op.into())
140 .vortex_expect("apply shouldn't fail")
141 .execute::<RecursiveCanonical>(ctx)
142 .map(|c| c.0.into_array());
143
144 let Ok(result) = result else {
147 continue;
148 };
149
150 let actual_values = to_vec_of_scalar(&result, ctx);
151
152 let expected_results: Vec<Option<Scalar>> = original_values
154 .iter()
155 .map(|x| {
156 x.as_primitive()
157 .checked_binary_numeric(&scalar_one.as_primitive(), op)
158 .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
159 })
160 .collect();
161
162 for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
164 if let Some(expected_value) = expected {
165 assert_eq!(
166 actual,
167 expected_value,
168 "Binary numeric operation failed for encoding {} at index {}: \
169 ({array:?})[{idx}] {operator:?} {scalar_one} \
170 expected {expected_value:?}, got {actual:?}",
171 array.encoding_id(),
172 idx,
173 );
174 }
175 }
176
177 let result = rhs_const.binary(array.clone(), op.into()).and_then(|a| {
179 a.execute::<RecursiveCanonical>(ctx)
180 .map(|c| c.0.into_array())
181 });
182
183 let Ok(result) = result else {
185 continue;
186 };
187
188 let actual_values = to_vec_of_scalar(&result, ctx);
189
190 let expected_results: Vec<Option<Scalar>> = original_values
192 .iter()
193 .map(|x| {
194 scalar_one
195 .as_primitive()
196 .checked_binary_numeric(&x.as_primitive(), op)
197 .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
198 })
199 .collect();
200
201 for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
203 if let Some(expected_value) = expected {
204 assert_eq!(
205 actual,
206 expected_value,
207 "Binary numeric operation failed for encoding {} at index {}: \
208 {scalar_one} {operator:?} ({array:?})[{idx}] \
209 expected {expected_value:?}, got {actual:?}",
210 array.encoding_id(),
211 idx,
212 );
213 }
214 }
215 }
216}
217
218pub fn test_binary_numeric_array(array: &ArrayRef, ctx: &mut ExecutionCtx) {
235 match array.dtype() {
236 DType::Primitive(ptype, _) => match ptype {
237 PType::I8 => test_binary_numeric_conformance::<i8>(array, ctx),
238 PType::I16 => test_binary_numeric_conformance::<i16>(array, ctx),
239 PType::I32 => test_binary_numeric_conformance::<i32>(array, ctx),
240 PType::I64 => test_binary_numeric_conformance::<i64>(array, ctx),
241 PType::U8 => test_binary_numeric_conformance::<u8>(array, ctx),
242 PType::U16 => test_binary_numeric_conformance::<u16>(array, ctx),
243 PType::U32 => test_binary_numeric_conformance::<u32>(array, ctx),
244 PType::U64 => test_binary_numeric_conformance::<u64>(array, ctx),
245 PType::F16 => {
246 eprintln!("Skipping f16 binary numeric tests (not supported)");
248 }
249 PType::F32 => test_binary_numeric_conformance::<f32>(array, ctx),
250 PType::F64 => test_binary_numeric_conformance::<f64>(array, ctx),
251 },
252 DType::Decimal(decimal_dtype, _) => {
253 test_binary_numeric_conformance_decimal(array, *decimal_dtype, ctx)
254 }
255 dtype => vortex_panic!(
256 "Binary numeric tests are only supported for primitive and decimal types, got {dtype}",
257 ),
258 }
259}
260
261fn test_binary_numeric_conformance_decimal(
265 array: &ArrayRef,
266 decimal_dtype: DecimalDType,
267 ctx: &mut ExecutionCtx,
268) {
269 let precision = decimal_dtype.precision() as usize;
270 let prec_max = <i256 as NativeDecimalType>::MAX_BY_PRECISION[precision];
271
272 let mut constants = vec![
273 i256::from_i128(0),
274 i256::from_i128(1),
275 i256::from_i128(-1),
276 prec_max,
277 ];
278 if decimal_dtype.scale() >= 0
280 && let Some(one) = i256::from_i128(10).checked_pow(decimal_dtype.scale() as u32)
281 && one <= prec_max
282 {
283 constants.push(one);
284 }
285
286 for constant in constants {
287 let value = DecimalValue::try_from_i256(constant, decimal_dtype)
288 .vortex_expect("conformance constants fit the precision");
289 test_decimal_binary_numeric_with_scalar(array, value, decimal_dtype, ctx);
290 }
291}
292
293fn test_decimal_binary_numeric_with_scalar(
294 array: &ArrayRef,
295 value: DecimalValue,
296 decimal_dtype: DecimalDType,
297 ctx: &mut ExecutionCtx,
298) {
299 let canonicalized_array = array
300 .clone()
301 .execute::<Canonical>(ctx)
302 .vortex_expect("Must be able to canonicalise")
303 .into_array();
304 let original_values = to_vec_of_scalar(&canonicalized_array, ctx);
305
306 let scalar = Scalar::decimal(value, decimal_dtype, array.dtype().nullability());
307
308 let mut operators = vec![
309 NumericOperator::Add,
310 NumericOperator::Sub,
311 NumericOperator::Mul,
312 ];
313 if !value.is_zero() {
314 operators.push(NumericOperator::Div);
315 }
316
317 for operator in operators {
318 for lhs_is_array in [true, false] {
319 test_decimal_binary_numeric_direction(
320 array,
321 &original_values,
322 &scalar,
323 decimal_dtype,
324 operator,
325 lhs_is_array,
326 ctx,
327 );
328 }
329 }
330}
331
332fn test_decimal_binary_numeric_direction(
333 array: &ArrayRef,
334 original_values: &[Scalar],
335 scalar: &Scalar,
336 decimal_dtype: DecimalDType,
337 operator: NumericOperator,
338 lhs_is_array: bool,
339 ctx: &mut ExecutionCtx,
340) {
341 let result_decimal_dtype = numeric_op_result_decimal_dtype(decimal_dtype, operator)
342 .vortex_expect("decimal arithmetic must have a result dtype");
343 let result_dtype = DType::Decimal(result_decimal_dtype, array.dtype().nullability());
344 let expected_results = expected_decimal_results(
345 original_values,
346 scalar,
347 operator,
348 result_decimal_dtype,
349 &result_dtype,
350 lhs_is_array,
351 );
352
353 let constant = ConstantArray::new(scalar.clone(), array.len()).into_array();
354 let (lhs, rhs) = if lhs_is_array {
355 (array.clone(), constant)
356 } else {
357 (constant, array.clone())
358 };
359 let result = lhs
360 .binary(rhs, operator.into())
361 .vortex_expect("binary decimal op shouldn't fail")
362 .execute::<RecursiveCanonical>(ctx)
363 .map(|c| c.0.into_array());
364
365 assert_decimal_results(
366 result,
367 &expected_results,
368 array,
369 scalar,
370 operator,
371 lhs_is_array,
372 ctx,
373 );
374}
375
376fn expected_decimal_results(
377 original_values: &[Scalar],
378 scalar: &Scalar,
379 operator: NumericOperator,
380 result_decimal_dtype: DecimalDType,
381 result_dtype: &DType,
382 lhs_is_array: bool,
383) -> Vec<Option<Scalar>> {
384 original_values
385 .iter()
386 .map(|value| {
387 let (lhs, rhs) = if lhs_is_array {
388 (value.as_decimal(), scalar.as_decimal())
389 } else {
390 (scalar.as_decimal(), value.as_decimal())
391 };
392 let (Some(lhs), Some(rhs)) = (lhs.decimal_value(), rhs.decimal_value()) else {
393 return Some(Scalar::null(result_dtype.clone()));
394 };
395 let lhs = lhs.as_i256();
396 let rhs = rhs.as_i256();
397 let value = match operator {
398 NumericOperator::Add => lhs.checked_add(&rhs),
399 NumericOperator::Sub => lhs.checked_sub(&rhs),
400 NumericOperator::Mul => lhs.checked_mul(&rhs),
401 NumericOperator::Div => {
402 let scale_power = result_decimal_dtype.scale();
403 let factor =
404 i256::from_i128(10).checked_pow(scale_power.unsigned_abs() as u32)?;
405 if scale_power >= 0 {
406 lhs.checked_mul(&factor)?.checked_div(&rhs)
407 } else {
408 lhs.checked_div(&rhs.checked_mul(&factor)?)
409 }
410 }
411 }?;
412 let value = DecimalValue::try_from_i256(value, result_decimal_dtype).ok()?;
413 Some(Scalar::decimal(
414 value,
415 result_decimal_dtype,
416 result_dtype.nullability(),
417 ))
418 })
419 .collect()
420}
421
422fn assert_decimal_results(
423 result: vortex_error::VortexResult<ArrayRef>,
424 expected_results: &[Option<Scalar>],
425 array: &ArrayRef,
426 scalar: &Scalar,
427 operator: NumericOperator,
428 lhs_is_array: bool,
429 ctx: &mut ExecutionCtx,
430) {
431 if expected_results.iter().any(Option::is_none) {
432 assert!(
433 result.is_err(),
434 "Decimal binary numeric operation should overflow for encoding {}: \
435 {operator:?} {scalar} (lhs_is_array: {lhs_is_array})",
436 array.encoding_id(),
437 );
438 return;
439 }
440
441 let result = result.unwrap_or_else(|err| {
442 vortex_panic!(
443 "Decimal binary numeric operation unexpectedly failed for encoding {}: \
444 {operator:?} {scalar} (lhs_is_array: {lhs_is_array}): {err}",
445 array.encoding_id(),
446 )
447 });
448
449 let actual_values = to_vec_of_scalar(&result, ctx);
450 for (idx, (actual, expected)) in actual_values.iter().zip(expected_results).enumerate() {
451 let expected_value = expected
452 .as_ref()
453 .vortex_expect("non-overflowing decimal lane must have an expected value");
454 assert_eq!(
455 actual,
456 expected_value,
457 "Decimal binary numeric operation failed for encoding {} at index {}: \
458 ({array:?})[{idx}] {operator:?} {scalar} (lhs_is_array: {lhs_is_array}) \
459 expected {expected_value:?}, got {actual:?}",
460 array.encoding_id(),
461 idx,
462 );
463 }
464}
465
466fn test_binary_numeric_edge_cases(array: &ArrayRef, ctx: &mut ExecutionCtx) {
474 match array.dtype() {
475 DType::Primitive(ptype, _) => match ptype {
476 PType::I8 => test_binary_numeric_edge_cases_signed::<i8>(array, ctx),
477 PType::I16 => test_binary_numeric_edge_cases_signed::<i16>(array, ctx),
478 PType::I32 => test_binary_numeric_edge_cases_signed::<i32>(array, ctx),
479 PType::I64 => test_binary_numeric_edge_cases_signed::<i64>(array, ctx),
480 PType::U8 => test_binary_numeric_edge_cases_unsigned::<u8>(array, ctx),
481 PType::U16 => test_binary_numeric_edge_cases_unsigned::<u16>(array, ctx),
482 PType::U32 => test_binary_numeric_edge_cases_unsigned::<u32>(array, ctx),
483 PType::U64 => test_binary_numeric_edge_cases_unsigned::<u64>(array, ctx),
484 PType::F16 => {
485 eprintln!("Skipping f16 edge case tests (not supported)");
486 }
487 PType::F32 => test_binary_numeric_edge_cases_float::<f32>(array, ctx),
488 PType::F64 => test_binary_numeric_edge_cases_float::<f64>(array, ctx),
489 },
490 dtype => vortex_panic!(
491 "Binary numeric edge case tests are only supported for primitive numeric types, got {dtype}"
492 ),
493 }
494}
495
496fn test_binary_numeric_edge_cases_signed<T>(array: &ArrayRef, ctx: &mut ExecutionCtx)
497where
498 T: NativePType + Num + Copy + Debug + Bounded + Signed,
499 Scalar: From<T>,
500{
501 test_binary_numeric_with_scalar(array, T::zero(), ctx);
503
504 test_binary_numeric_with_scalar(array, -T::one(), ctx);
506
507 test_binary_numeric_with_scalar(array, T::max_value(), ctx);
509
510 test_binary_numeric_with_scalar(array, T::min_value(), ctx);
512}
513
514fn test_binary_numeric_edge_cases_unsigned<T>(array: &ArrayRef, ctx: &mut ExecutionCtx)
515where
516 T: NativePType + Num + Copy + Debug + Bounded,
517 Scalar: From<T>,
518{
519 test_binary_numeric_with_scalar(array, T::zero(), ctx);
521
522 test_binary_numeric_with_scalar(array, T::max_value(), ctx);
524}
525
526fn test_binary_numeric_edge_cases_float<T>(array: &ArrayRef, ctx: &mut ExecutionCtx)
527where
528 T: NativePType + Num + Copy + Debug + Float,
529 Scalar: From<T>,
530{
531 test_binary_numeric_with_scalar(array, T::zero(), ctx);
533
534 test_binary_numeric_with_scalar(array, -T::one(), ctx);
536
537 test_binary_numeric_with_scalar(array, T::max_value(), ctx);
539
540 test_binary_numeric_with_scalar(array, T::min_value(), ctx);
542
543 test_binary_numeric_with_scalar(array, T::epsilon(), ctx);
545
546 test_binary_numeric_with_scalar(array, T::min_positive_value(), ctx);
548
549 test_binary_numeric_with_scalar(array, T::nan(), ctx);
551 test_binary_numeric_with_scalar(array, T::infinity(), ctx);
552 test_binary_numeric_with_scalar(array, T::neg_infinity(), ctx);
553}
554
555fn test_binary_numeric_with_scalar<T>(array: &ArrayRef, scalar_value: T, ctx: &mut ExecutionCtx)
556where
557 T: NativePType + Num + Copy + Debug,
558 Scalar: From<T>,
559{
560 let canonicalized_array = array
561 .clone()
562 .execute::<Canonical>(ctx)
563 .vortex_expect("Must be able to canonicalise")
564 .into_array();
565 let original_values = to_vec_of_scalar(&canonicalized_array, ctx);
566
567 let scalar = Scalar::from(scalar_value)
568 .cast(array.dtype())
569 .vortex_expect("operation should succeed in conformance test");
570
571 let operators = if scalar_value == T::zero() {
573 vec![
575 NumericOperator::Add,
576 NumericOperator::Sub,
577 NumericOperator::Mul,
578 ]
579 } else {
580 vec![
581 NumericOperator::Add,
582 NumericOperator::Sub,
583 NumericOperator::Mul,
584 NumericOperator::Div,
585 ]
586 };
587
588 for operator in operators {
589 let op = operator;
590 let rhs_const = ConstantArray::new(scalar.clone(), array.len()).into_array();
591
592 let result = array
594 .binary(rhs_const, op.into())
595 .vortex_expect("apply failed")
596 .execute::<RecursiveCanonical>(ctx)
597 .map(|x| x.0.into_array());
598
599 if result.is_err() {
602 continue;
603 }
604
605 let result = result.vortex_expect("operation should succeed in conformance test");
606 let actual_values = to_vec_of_scalar(&result, ctx);
607
608 let expected_results: Vec<Option<Scalar>> = original_values
610 .iter()
611 .map(|x| {
612 x.as_primitive()
613 .checked_binary_numeric(&scalar.as_primitive(), op)
614 .map(<Scalar as From<PrimitiveScalar<'_>>>::from)
615 })
616 .collect();
617
618 for (idx, (actual, expected)) in actual_values.iter().zip(&expected_results).enumerate() {
620 if let Some(expected_value) = expected {
621 assert_eq!(
622 actual,
623 expected_value,
624 "Binary numeric operation failed for encoding {} at index {} with scalar {:?}: \
625 ({array:?})[{idx}] {operator:?} {scalar} \
626 expected {expected_value:?}, got {actual:?}",
627 array.encoding_id(),
628 idx,
629 scalar_value,
630 );
631 }
632 }
633 }
634}