From 8f96a33d8c57731c0d9f15ba486fac88a7c15b29 Mon Sep 17 00:00:00 2001 From: Peter Bower <37089506+pbower@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:15:32 +0100 Subject: [PATCH 1/3] Apply the requested operator to scalar-scalar broadcast_value --- src/kernels/arithmetic/types.rs | 33 +++++++++++++++++++++++++++++++++ src/kernels/broadcast/mod.rs | 2 +- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/kernels/arithmetic/types.rs b/src/kernels/arithmetic/types.rs index 1895674f..649cfc0d 100644 --- a/src/kernels/arithmetic/types.rs +++ b/src/kernels/arithmetic/types.rs @@ -1072,6 +1072,39 @@ mod tests { } } + #[cfg(feature = "scalar_type")] + #[test] + fn scalar_operands_apply_the_requested_operator() { + use crate::Scalar; + use crate::enums::operators::ArithmeticOperator; + use crate::kernels::broadcast::broadcast_value; + + let apply = |op| { + broadcast_value( + op, + Value::Scalar(Scalar::Int32(7)), + Value::Scalar(Scalar::Int32(2)), + ) + .unwrap() + }; + + let Value::Scalar(Scalar::Int32(diff)) = apply(ArithmeticOperator::Subtract) else { + panic!("Expected Int32 scalar from subtraction"); + }; + assert_eq!(diff, 5); + + let Value::Scalar(Scalar::Int32(prod)) = apply(ArithmeticOperator::Multiply) else { + panic!("Expected Int32 scalar from multiplication"); + }; + assert_eq!(prod, 14); + + // Integer scalar division is true division with a Float64 result. + let Value::Scalar(Scalar::Float64(quot)) = apply(ArithmeticOperator::Divide) else { + panic!("Expected Float64 scalar from division"); + }; + assert_eq!(quot, 3.5); + } + #[test] fn test_reference_operations() { let arr1 = diff --git a/src/kernels/broadcast/mod.rs b/src/kernels/broadcast/mod.rs index f2c39ea7..a0a062c7 100644 --- a/src/kernels/broadcast/mod.rs +++ b/src/kernels/broadcast/mod.rs @@ -276,7 +276,7 @@ pub fn broadcast_value( // Scalar + Scalar = Scalar #[cfg(feature = "scalar_type")] (Value::Scalar(l), Value::Scalar(r)) => { - scalar_arithmetic(l, r, ArithmeticOperator::Add).map(Value::Scalar) + scalar_arithmetic(l, r, op).map(Value::Scalar) } // Array types - use resolve_binary_arithmetic From e3e19e15e82fee7ba58b053fdcc955c82332d265 Mon Sep 17 00:00:00 2001 From: Peter Bower <37089506+pbower@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:34:26 +0100 Subject: [PATCH 2/3] Add remainder, power and floor division to scalar arithmetic --- src/kernels/arithmetic/types.rs | 65 ++++++++++++++++++++++++++++ src/kernels/routing/arithmetic.rs | 70 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/kernels/arithmetic/types.rs b/src/kernels/arithmetic/types.rs index 649cfc0d..e1d7f8d8 100644 --- a/src/kernels/arithmetic/types.rs +++ b/src/kernels/arithmetic/types.rs @@ -1103,6 +1103,71 @@ mod tests { panic!("Expected Float64 scalar from division"); }; assert_eq!(quot, 3.5); + + let Value::Scalar(Scalar::Int32(rem)) = apply(ArithmeticOperator::Remainder) else { + panic!("Expected Int32 scalar from remainder"); + }; + assert_eq!(rem, 1); + + let Value::Scalar(Scalar::Int32(pow)) = apply(ArithmeticOperator::Power) else { + panic!("Expected Int32 scalar from power"); + }; + assert_eq!(pow, 49); + + let Value::Scalar(Scalar::Int32(floor)) = apply(ArithmeticOperator::FloorDiv) else { + panic!("Expected Int32 scalar from floor division"); + }; + assert_eq!(floor, 3); + + // Remainder keeps the dividend's sign and floor division rounds + // towards negative infinity. + let negative = |op| { + broadcast_value( + op, + Value::Scalar(Scalar::Int32(-7)), + Value::Scalar(Scalar::Int32(2)), + ) + .unwrap() + }; + let Value::Scalar(Scalar::Int32(rem)) = negative(ArithmeticOperator::Remainder) else { + panic!("Expected Int32 scalar from remainder"); + }; + assert_eq!(rem, -1); + let Value::Scalar(Scalar::Int32(floor)) = negative(ArithmeticOperator::FloorDiv) else { + panic!("Expected Int32 scalar from floor division"); + }; + assert_eq!(floor, -4); + + let float = |op| { + broadcast_value( + op, + Value::Scalar(Scalar::Float64(7.5)), + Value::Scalar(Scalar::Float64(2.0)), + ) + .unwrap() + }; + let Value::Scalar(Scalar::Float64(rem)) = float(ArithmeticOperator::Remainder) else { + panic!("Expected Float64 scalar from remainder"); + }; + assert_eq!(rem, 1.5); + let Value::Scalar(Scalar::Float64(pow)) = float(ArithmeticOperator::Power) else { + panic!("Expected Float64 scalar from power"); + }; + assert!((pow - 56.25).abs() < 1e-9); + let Value::Scalar(Scalar::Float64(floor)) = float(ArithmeticOperator::FloorDiv) else { + panic!("Expected Float64 scalar from floor division"); + }; + assert_eq!(floor, 3.0); + + // A zero divisor and a negative integer exponent return errors. + let zero = Value::Scalar(Scalar::Int32(0)); + let seven = Value::Scalar(Scalar::Int32(7)); + let negative_one = Value::Scalar(Scalar::Int32(-1)); + assert!( + broadcast_value(ArithmeticOperator::Remainder, seven.clone(), zero.clone()).is_err() + ); + assert!(broadcast_value(ArithmeticOperator::FloorDiv, seven.clone(), zero).is_err()); + assert!(broadcast_value(ArithmeticOperator::Power, seven, negative_one).is_err()); } #[test] diff --git a/src/kernels/routing/arithmetic.rs b/src/kernels/routing/arithmetic.rs index 1e464a0c..9bca541f 100644 --- a/src/kernels/routing/arithmetic.rs +++ b/src/kernels/routing/arithmetic.rs @@ -28,6 +28,10 @@ use crate::kernels::arithmetic::{ }, string_ops::apply_str_str, }; +#[cfg(feature = "scalar_type")] +use crate::kernels::arithmetic::std::int_dense_body_std; +#[cfg(feature = "scalar_type")] +use num_traits::ToPrimitive; #[cfg(feature = "decimal")] use crate::kernels::arithmetic::decimal::{decimal_binary, integer_to_decimal, max_precision}; @@ -43,6 +47,37 @@ pub fn scalar_arithmetic( use ArithmeticOperator::*; use Scalar; + // Applies `Remainder`, `Power` or `FloorDiv` to two integer scalars through + // the dense integer kernel, matching the integer array results. + // + // ## Behaviour + // - remainder keeps the dividend's sign + // - floor division rounds towards negative infinity and keeps the integer type + // - power uses wrapping exponentiation by squaring + // - a zero divisor, an overflowing division, or an exponent outside `u32` + // returns an error, ahead of the kernel's panic on those inputs. + macro_rules! integer_scalar_op { + ($l:expr, $r:expr, $op:expr) => {{ + let (l, r, op) = ($l, $r, $op); + if op == Power { + if r.to_u32().is_none() { + return Err(MinarrowError::KernelError(Some(format!( + "Power exponent {} is outside the u32 range. Consider casting to float first.", + r + )))); + } + } else if l.checked_rem(r).is_none() { + return Err(MinarrowError::KernelError(Some(format!( + "Integer {:?} of {} by {} has a zero divisor or overflows", + op, l, r + )))); + } + let mut out = [l]; + int_dense_body_std(op, &[l], &[r], &mut out); + out[0] + }}; + } + let result = match (lhs, rhs, op) { // Int32 operations (Scalar::Int32(l), Scalar::Int32(r), Add) => Scalar::Int32(l + r), @@ -51,24 +86,37 @@ pub fn scalar_arithmetic( // Integer scalar division is true division and returns a Float64 // scalar, so 7 / 2 is 3.5 and division by zero follows IEEE 754. (Scalar::Int32(l), Scalar::Int32(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::Int32(l), Scalar::Int32(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int32(integer_scalar_op!(l, r, op)) + } // Int64 operations (Scalar::Int64(l), Scalar::Int64(r), Add) => Scalar::Int64(l + r), (Scalar::Int64(l), Scalar::Int64(r), Subtract) => Scalar::Int64(l - r), (Scalar::Int64(l), Scalar::Int64(r), Multiply) => Scalar::Int64(l * r), (Scalar::Int64(l), Scalar::Int64(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::Int64(l), Scalar::Int64(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int64(integer_scalar_op!(l, r, op)) + } // Float32 operations (Scalar::Float32(l), Scalar::Float32(r), Add) => Scalar::Float32(l + r), (Scalar::Float32(l), Scalar::Float32(r), Subtract) => Scalar::Float32(l - r), (Scalar::Float32(l), Scalar::Float32(r), Multiply) => Scalar::Float32(l * r), (Scalar::Float32(l), Scalar::Float32(r), Divide) => Scalar::Float32(l / r), + (Scalar::Float32(l), Scalar::Float32(r), Remainder) => Scalar::Float32(l % r), + // Float power uses logarithmic exponentiation, matching the float array kernels. + (Scalar::Float32(l), Scalar::Float32(r), Power) => Scalar::Float32((r * l.ln()).exp()), + (Scalar::Float32(l), Scalar::Float32(r), FloorDiv) => Scalar::Float32((l / r).floor()), // Float64 operations (Scalar::Float64(l), Scalar::Float64(r), Add) => Scalar::Float64(l + r), (Scalar::Float64(l), Scalar::Float64(r), Subtract) => Scalar::Float64(l - r), (Scalar::Float64(l), Scalar::Float64(r), Multiply) => Scalar::Float64(l * r), (Scalar::Float64(l), Scalar::Float64(r), Divide) => Scalar::Float64(l / r), + (Scalar::Float64(l), Scalar::Float64(r), Remainder) => Scalar::Float64(l % r), + (Scalar::Float64(l), Scalar::Float64(r), Power) => Scalar::Float64((r * l.ln()).exp()), + (Scalar::Float64(l), Scalar::Float64(r), FloorDiv) => Scalar::Float64((l / r).floor()), // Mixed type promotion (Int + Float = Float) (Scalar::Int32(l), Scalar::Float32(r), op) => { @@ -93,6 +141,10 @@ pub fn scalar_arithmetic( (Scalar::Int8(l), Scalar::Int8(r), Multiply) => Scalar::Int8(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::Int8(l), Scalar::Int8(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::Int8(l), Scalar::Int8(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int8(integer_scalar_op!(l, r, op)) + } // Int16 #[cfg(feature = "extended_numeric_types")] @@ -103,6 +155,10 @@ pub fn scalar_arithmetic( (Scalar::Int16(l), Scalar::Int16(r), Multiply) => Scalar::Int16(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::Int16(l), Scalar::Int16(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::Int16(l), Scalar::Int16(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::Int16(integer_scalar_op!(l, r, op)) + } // UInt8 #[cfg(feature = "extended_numeric_types")] @@ -113,6 +169,10 @@ pub fn scalar_arithmetic( (Scalar::UInt8(l), Scalar::UInt8(r), Multiply) => Scalar::UInt8(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::UInt8(l), Scalar::UInt8(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::UInt8(l), Scalar::UInt8(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt8(integer_scalar_op!(l, r, op)) + } // UInt16 #[cfg(feature = "extended_numeric_types")] @@ -123,18 +183,28 @@ pub fn scalar_arithmetic( (Scalar::UInt16(l), Scalar::UInt16(r), Multiply) => Scalar::UInt16(l * r), #[cfg(feature = "extended_numeric_types")] (Scalar::UInt16(l), Scalar::UInt16(r), Divide) => Scalar::Float64(l as f64 / r as f64), + #[cfg(feature = "extended_numeric_types")] + (Scalar::UInt16(l), Scalar::UInt16(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt16(integer_scalar_op!(l, r, op)) + } // UInt32 (Scalar::UInt32(l), Scalar::UInt32(r), Add) => Scalar::UInt32(l + r), (Scalar::UInt32(l), Scalar::UInt32(r), Subtract) => Scalar::UInt32(l - r), (Scalar::UInt32(l), Scalar::UInt32(r), Multiply) => Scalar::UInt32(l * r), (Scalar::UInt32(l), Scalar::UInt32(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::UInt32(l), Scalar::UInt32(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt32(integer_scalar_op!(l, r, op)) + } // UInt64 (Scalar::UInt64(l), Scalar::UInt64(r), Add) => Scalar::UInt64(l + r), (Scalar::UInt64(l), Scalar::UInt64(r), Subtract) => Scalar::UInt64(l - r), (Scalar::UInt64(l), Scalar::UInt64(r), Multiply) => Scalar::UInt64(l * r), (Scalar::UInt64(l), Scalar::UInt64(r), Divide) => Scalar::Float64(l as f64 / r as f64), + (Scalar::UInt64(l), Scalar::UInt64(r), op @ (Remainder | Power | FloorDiv)) => { + Scalar::UInt64(integer_scalar_op!(l, r, op)) + } // String concatenation (Scalar::String32(l), Scalar::String32(r), Add) => Scalar::String32(format!("{}{}", l, r)), #[cfg(feature = "large_string")] From f7fa7e36497c25d64340e035f2a477f961ec447e Mon Sep 17 00:00:00 2001 From: Peter Bower <37089506+pbower@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:16:58 +0100 Subject: [PATCH 3/3] Update arithmetic.rs --- src/kernels/routing/arithmetic.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/kernels/routing/arithmetic.rs b/src/kernels/routing/arithmetic.rs index 9bca541f..421e8c50 100644 --- a/src/kernels/routing/arithmetic.rs +++ b/src/kernels/routing/arithmetic.rs @@ -47,15 +47,16 @@ pub fn scalar_arithmetic( use ArithmeticOperator::*; use Scalar; - // Applies `Remainder`, `Power` or `FloorDiv` to two integer scalars through - // the dense integer kernel, matching the integer array results. + // Computes `Remainder`, `Power` or `FloorDiv` for two integer scalars with the + // integer array kernel `int_dense_body_std`, applied to one-element slices. // // ## Behaviour - // - remainder keeps the dividend's sign - // - floor division rounds towards negative infinity and keeps the integer type - // - power uses wrapping exponentiation by squaring - // - a zero divisor, an overflowing division, or an exponent outside `u32` - // returns an error, ahead of the kernel's panic on those inputs. + // - Remainder keeps the dividend's sign. + // - Floor division rounds towards negative infinity and keeps the integer type. + // - Power uses wrapping exponentiation by squaring. + // - The kernel panics on a zero divisor, an overflowing division and an + // exponent outside `u32`. These inputs are checked first and return + // `MinarrowError::KernelError`. macro_rules! integer_scalar_op { ($l:expr, $r:expr, $op:expr) => {{ let (l, r, op) = ($l, $r, $op);