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