From 91343d7bf603cc7e54ad4a5cb4ba88246c9ad9e8 Mon Sep 17 00:00:00 2001 From: Nicholas Gates Date: Tue, 20 Jan 2026 20:53:15 +0000 Subject: [PATCH] Expression::execute Signed-off-by: Nicholas Gates --- vortex-array/src/expr/exprs/between.rs | 32 +++--- vortex-array/src/expr/exprs/binary.rs | 94 ++++------------- vortex-array/src/expr/exprs/cast.rs | 12 ++- vortex-array/src/expr/exprs/dynamic.rs | 25 ++--- vortex-array/src/expr/exprs/get_item.rs | 42 ++++---- vortex-array/src/expr/exprs/is_null.rs | 31 +++--- vortex-array/src/expr/exprs/like.rs | 17 +++- vortex-array/src/expr/scalar_fn.rs | 3 +- vortex-array/src/expr/vtable.rs | 129 ++++++++++++++++-------- 9 files changed, 189 insertions(+), 196 deletions(-) diff --git a/vortex-array/src/expr/exprs/between.rs b/vortex-array/src/expr/exprs/between.rs index 67d345755ed..48a2a52ad83 100644 --- a/vortex-array/src/expr/exprs/between.rs +++ b/vortex-array/src/expr/exprs/between.rs @@ -11,14 +11,15 @@ use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_error::vortex_err; use vortex_proto::expr as pb; -use vortex_vector::Datum; use crate::ArrayRef; +use crate::IntoArray; use crate::compute::BetweenOptions; use crate::compute::between as between_compute; use crate::expr::Arity; use crate::expr::ChildName; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::StatsCatalog; use crate::expr::VTable; @@ -150,38 +151,35 @@ impl VTable for Between { between_compute(&arr, &lower, &upper, options) } - fn execute(&self, options: &Self::Options, args: ExecutionArgs) -> VortexResult { - let [arr, lower, upper]: [Datum; _] = args - .datums + fn execute( + &self, + options: &Self::Options, + args: ExecutionArgs, + ) -> VortexResult { + let [arr, lower, upper]: [ArrayRef; _] = args + .inputs .try_into() .map_err(|_| vortex_err!("Expected 3 arguments for Between expression",))?; - let [arr_dt, lower_dt, upper_dt]: [DType; _] = args - .dtypes - .try_into() - .map_err(|_| vortex_err!("Expected 3 dtypes for Between expression",))?; let lower_bound = Binary .bind(options.lower_strict.to_operator().into()) .execute(ExecutionArgs { - datums: vec![lower, arr.clone()], - dtypes: vec![lower_dt, arr_dt.clone()], + inputs: vec![lower.clone(), arr.clone()], row_count: args.row_count, - return_dtype: args.return_dtype.clone(), + ctx: args.ctx, })?; let upper_bound = Binary .bind(options.upper_strict.to_operator().into()) .execute(ExecutionArgs { - datums: vec![arr, upper], - dtypes: vec![arr_dt, upper_dt], + inputs: vec![arr, upper], row_count: args.row_count, - return_dtype: args.return_dtype.clone(), + ctx: args.ctx, })?; Binary.bind(Operator::And).execute(ExecutionArgs { - datums: vec![lower_bound, upper_bound], - dtypes: vec![args.return_dtype.clone(), args.return_dtype.clone()], + inputs: vec![lower_bound.into_array(), upper_bound.into_array()], row_count: args.row_count, - return_dtype: args.return_dtype, + ctx: args.ctx, }) } diff --git a/vortex-array/src/expr/exprs/binary.rs b/vortex-array/src/expr/exprs/binary.rs index 95e408321c0..4727c78c63d 100644 --- a/vortex-array/src/expr/exprs/binary.rs +++ b/vortex-array/src/expr/exprs/binary.rs @@ -31,6 +31,7 @@ use crate::compute::sub; use crate::expr::Arity; use crate::expr::ChildName; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::StatsCatalog; use crate::expr::VTable; @@ -131,86 +132,27 @@ impl VTable for Binary { } } - fn execute(&self, op: &Operator, args: ExecutionArgs) -> VortexResult { - let [lhs, rhs]: [Datum; _] = args - .datums + fn execute(&self, op: &Operator, args: ExecutionArgs) -> VortexResult { + let [lhs, rhs]: [ArrayRef; _] = args + .inputs .try_into() .map_err(|_| vortex_err!("Wrong arg count"))?; - // Handle logical operators. match op { - Operator::And => { - return Ok(LogicalAndKleene::and_kleene(&lhs.into_bool(), &rhs.into_bool()).into()); - } - Operator::Or => { - return Ok(LogicalOrKleene::or_kleene(&lhs.into_bool(), &rhs.into_bool()).into()); - } - _ => {} - } - - // Arrow's vectorized comparison kernels (`cmp::eq`, etc.) don't support nested types - // (Struct, List, FixedSizeList). For those, we use `compare_nested_arrow_arrays` which does - // element-wise comparison via `make_comparator`. - if let Some(cmp_op) = op.maybe_cmp_operator() - && (lhs.is_nested() || rhs.is_nested()) - { - // Treat scalars as 1-element arrow arrays. - let lhs_arr = lhs.into_arrow()?; - let rhs_arr = rhs.into_arrow()?; - - let bool_array = compare_nested_arrow_arrays(lhs_arr.get().0, rhs_arr.get().0, cmp_op)?; - let vector = bool_array.into_vector()?; - - let both_are_scalar = lhs_arr.get().1 && rhs_arr.get().1; - - return Ok(if both_are_scalar { - Datum::Scalar(vortex_vector::Scalar::Bool(vector.scalar_at(0))) - } else { - Datum::Vector(vortex_vector::Vector::Bool(vector)) - }); - } - - let lhs = lhs.into_arrow()?; - let rhs = rhs.into_arrow()?; - - let vector = match op { - // Handle comparison operators. - Operator::Eq => cmp::eq(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(), - Operator::NotEq => cmp::neq(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(), - Operator::Gt => cmp::gt(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(), - Operator::Gte => cmp::gt_eq(lhs.as_ref(), rhs.as_ref())? - .into_vector()? - .into(), - Operator::Lt => cmp::lt(lhs.as_ref(), rhs.as_ref())?.into_vector()?.into(), - Operator::Lte => cmp::lt_eq(lhs.as_ref(), rhs.as_ref())? - .into_vector()? - .into(), - - // Handle arithmetic operators. - Operator::Add => { - arrow_arith::numeric::add(lhs.as_ref(), rhs.as_ref())?.into_vector()? - } - Operator::Sub => { - arrow_arith::numeric::sub(lhs.as_ref(), rhs.as_ref())?.into_vector()? - } - Operator::Mul => { - arrow_arith::numeric::mul(lhs.as_ref(), rhs.as_ref())?.into_vector()? - } - Operator::Div => { - arrow_arith::numeric::div(lhs.as_ref(), rhs.as_ref())?.into_vector()? - } - - // Logical operators were handled above. - Operator::And | Operator::Or => unreachable!("Already dealt with above"), - }; - - let both_are_scalar = lhs.get().1 && rhs.get().1; - - Ok(if both_are_scalar { - Datum::Scalar(vector.scalar_at(0)) - } else { - Datum::Vector(vector) - }) + Operator::Eq => compare(&lhs, &rhs, compute::Operator::Eq), + Operator::NotEq => compare(&lhs, &rhs, compute::Operator::NotEq), + Operator::Lt => compare(&lhs, &rhs, compute::Operator::Lt), + Operator::Lte => compare(&lhs, &rhs, compute::Operator::Lte), + Operator::Gt => compare(&lhs, &rhs, compute::Operator::Gt), + Operator::Gte => compare(&lhs, &rhs, compute::Operator::Gte), + Operator::And => and_kleene(&lhs, &rhs), + Operator::Or => or_kleene(&lhs, &rhs), + Operator::Add => add(&lhs, &rhs), + Operator::Sub => sub(&lhs, &rhs), + Operator::Mul => mul(&lhs, &rhs), + Operator::Div => div(&lhs, &rhs), + }? + .execute(args.ctx) } fn stat_falsification( diff --git a/vortex-array/src/expr/exprs/cast.rs b/vortex-array/src/expr/exprs/cast.rs index 3c9bce98028..82ab1d76865 100644 --- a/vortex-array/src/expr/exprs/cast.rs +++ b/vortex-array/src/expr/exprs/cast.rs @@ -10,13 +10,13 @@ use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_err; use vortex_proto::expr as pb; -use vortex_vector::Datum; use crate::ArrayRef; use crate::compute::cast as compute_cast; use crate::expr::Arity; use crate::expr::ChildName; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::ReduceCtx; use crate::expr::ReduceNode; @@ -93,12 +93,16 @@ impl VTable for Cast { }) } - fn execute(&self, target_dtype: &DType, mut args: ExecutionArgs) -> VortexResult { + fn execute( + &self, + target_dtype: &DType, + mut args: ExecutionArgs, + ) -> VortexResult { let input = args - .datums + .inputs .pop() .vortex_expect("missing input for Cast expression"); - vortex_compute::cast::Cast::cast(&input, target_dtype) + compute_cast(input.as_ref(), target_dtype)?.execute(args.ctx) } fn reduce( diff --git a/vortex-array/src/expr/exprs/dynamic.rs b/vortex-array/src/expr/exprs/dynamic.rs index 39fc91cec9a..89f8f57d342 100644 --- a/vortex-array/src/expr/exprs/dynamic.rs +++ b/vortex-array/src/expr/exprs/dynamic.rs @@ -15,9 +15,6 @@ use vortex_error::VortexResult; use vortex_error::vortex_bail; use vortex_scalar::Scalar; use vortex_scalar::ScalarValue; -use vortex_vector::Datum; -use vortex_vector::Scalar as VectorScalar; -use vortex_vector::bool::BoolScalar; use crate::Array; use crate::ArrayRef; @@ -29,6 +26,7 @@ use crate::expr::Arity; use crate::expr::Binary; use crate::expr::ChildName; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::Expression; use crate::expr::StatsCatalog; @@ -118,26 +116,25 @@ impl VTable for DynamicComparison { .into_array()) } - fn execute(&self, data: &Self::Options, args: ExecutionArgs) -> VortexResult { + fn execute(&self, data: &Self::Options, args: ExecutionArgs) -> VortexResult { if let Some(scalar) = data.rhs.scalar() { - let [lhs]: [Datum; _] = args - .datums + let [lhs]: [ArrayRef; _] = args + .inputs .try_into() .map_err(|_| vortex_error::vortex_err!("Wrong arg count for DynamicComparison"))?; - let rhs_vector_scalar = scalar.to_vector_scalar(); - let rhs = Datum::Scalar(rhs_vector_scalar); + let rhs = ConstantArray::new(scalar.clone(), args.row_count).into_array(); return Binary.bind(data.operator.into()).execute(ExecutionArgs { - datums: vec![lhs, rhs], - dtypes: args.dtypes, + inputs: vec![lhs, rhs], row_count: args.row_count, - return_dtype: args.return_dtype, + ctx: args.ctx, }); } - Ok(Datum::Scalar(VectorScalar::Bool(BoolScalar::new(Some( - data.default, - ))))) + Ok(ExecutionResult::Scalar(ConstantArray::new( + false, + args.row_count, + ))) } fn stat_falsification( diff --git a/vortex-array/src/expr/exprs/get_item.rs b/vortex-array/src/expr/exprs/get_item.rs index 4437004040b..0f338ba78f9 100644 --- a/vortex-array/src/expr/exprs/get_item.rs +++ b/vortex-array/src/expr/exprs/get_item.rs @@ -13,18 +13,17 @@ use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_err; use vortex_proto::expr as pb; -use vortex_vector::Datum; -use vortex_vector::ScalarOps; -use vortex_vector::VectorOps; use crate::ArrayRef; use crate::ToCanonical; +use crate::arrays::StructArray; use crate::builtins::ExprBuiltins; use crate::compute::mask; use crate::expr::Arity; use crate::expr::ChildName; use crate::expr::EmptyOptions; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::Expression; use crate::expr::Literal; @@ -119,26 +118,23 @@ impl VTable for GetItem { } } - fn execute(&self, field_name: &FieldName, mut args: ExecutionArgs) -> VortexResult { - let struct_dtype = args.dtypes[0] - .as_struct_fields_opt() - .ok_or_else(|| vortex_err!("Expected struct dtype for child of GetItem expression"))?; - let field_idx = struct_dtype - .find(field_name) - .ok_or_else(|| vortex_err!("Field {} not found in struct dtype", field_name))?; - - match args.datums.pop().vortex_expect("missing input") { - Datum::Scalar(s) => { - let mut field = s.as_struct().field(field_idx); - field.mask_validity(s.is_valid()); - Ok(Datum::Scalar(field)) - } - Datum::Vector(v) => { - let mut field = v.as_struct().fields()[field_idx].clone(); - field.mask_validity(v.validity()); - Ok(Datum::Vector(field)) - } - } + fn execute( + &self, + field_name: &FieldName, + mut args: ExecutionArgs, + ) -> VortexResult { + let input = args + .inputs + .pop() + .vortex_expect("missing input for GetItem expression") + .execute::(args.ctx)?; + let field = input.field_by_name(field_name).cloned()?; + + match input.dtype().nullability() { + Nullability::NonNullable => Ok(field), + Nullability::Nullable => mask(&field, &input.validity_mask().not()), + }? + .execute(args.ctx) } fn reduce( diff --git a/vortex-array/src/expr/exprs/is_null.rs b/vortex-array/src/expr/exprs/is_null.rs index da02d823c28..d5eab1d1245 100644 --- a/vortex-array/src/expr/exprs/is_null.rs +++ b/vortex-array/src/expr/exprs/is_null.rs @@ -2,18 +2,11 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use std::fmt::Formatter; -use std::ops::Not; use vortex_dtype::DType; use vortex_dtype::Nullability; use vortex_error::VortexExpect; use vortex_error::VortexResult; -use vortex_mask::Mask; -use vortex_vector::Datum; -use vortex_vector::ScalarOps; -use vortex_vector::VectorOps; -use vortex_vector::bool::BoolScalar; -use vortex_vector::bool::BoolVector; use crate::Array; use crate::ArrayRef; @@ -24,6 +17,7 @@ use crate::expr::Arity; use crate::expr::ChildName; use crate::expr::EmptyOptions; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::Expression; use crate::expr::StatsCatalog; @@ -94,13 +88,22 @@ impl VTable for IsNull { }) } - fn execute(&self, _data: &Self::Options, mut args: ExecutionArgs) -> VortexResult { - let child = args.datums.pop().vortex_expect("Missing input child"); - Ok(match child { - Datum::Scalar(s) => Datum::Scalar(BoolScalar::new(Some(s.is_null())).into()), - Datum::Vector(v) => Datum::Vector( - BoolVector::new(v.validity().to_bit_buffer().not(), Mask::new_true(v.len())).into(), - ), + fn execute( + &self, + _data: &Self::Options, + mut args: ExecutionArgs, + ) -> VortexResult { + let child = args.inputs.pop().vortex_expect("Missing input child"); + if let Some(scalar) = child.as_constant() { + return Ok(ExecutionResult::constant(scalar.is_null(), args.row_count)); + } + + Ok(match child.validity()? { + Validity::NonNullable | Validity::AllValid => { + ExecutionResult::constant(false, args.row_count) + } + Validity::AllInvalid => ExecutionResult::constant(true, args.row_count), + Validity::Array(a) => a.not()?.execute(args.ctx)?, }) } diff --git a/vortex-array/src/expr/exprs/like.rs b/vortex-array/src/expr/exprs/like.rs index 43ea22226b3..b1ba948eb1c 100644 --- a/vortex-array/src/expr/exprs/like.rs +++ b/vortex-array/src/expr/exprs/like.rs @@ -15,11 +15,13 @@ use vortex_vector::Datum; use vortex_vector::VectorOps; use crate::ArrayRef; +use crate::arrow::ArrowArrayExecutor; use crate::compute::LikeOptions; use crate::compute::like as like_compute; use crate::expr::Arity; use crate::expr::ChildName; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::Expression; use crate::expr::VTable; @@ -114,14 +116,19 @@ impl VTable for Like { like_compute(&child, &pattern, *options) } - fn execute(&self, options: &Self::Options, args: ExecutionArgs) -> VortexResult { - let [child, pattern]: [Datum; _] = args - .datums + fn execute( + &self, + options: &Self::Options, + args: ExecutionArgs, + ) -> VortexResult { + let [child, pattern]: [ArrayRef; _] = args + .inputs .try_into() .map_err(|_| vortex_err!("Wrong argument count"))?; - let child = child.into_arrow()?; - let pattern = pattern.into_arrow()?; + // FIXME(ngates): we need to execute into an Arrow Datum. + let child = child.execute_arrow(None, args.ctx)?; + let pattern = pattern.execute_arrow(None, args.ctx)?; let array = match (options.negated, options.case_insensitive) { (false, false) => arrow_string::like::like(child.as_ref(), pattern.as_ref()), diff --git a/vortex-array/src/expr/scalar_fn.rs b/vortex-array/src/expr/scalar_fn.rs index 575d476ba72..538e15f864b 100644 --- a/vortex-array/src/expr/scalar_fn.rs +++ b/vortex-array/src/expr/scalar_fn.rs @@ -18,6 +18,7 @@ use vortex_vector::Datum; use crate::ArrayRef; use crate::expr::EmptyOptions; use crate::expr::ExecutionArgs; +use crate::expr::ExecutionResult; use crate::expr::ExprId; use crate::expr::ExprVTable; use crate::expr::Expression; @@ -138,7 +139,7 @@ impl ScalarFn { } /// Execute the expression given the input arguments. - pub fn execute(&self, ctx: ExecutionArgs) -> VortexResult { + pub fn execute(&self, ctx: ExecutionArgs) -> VortexResult { self.vtable.as_dyn().execute(self.options.deref(), ctx) } diff --git a/vortex-array/src/expr/vtable.rs b/vortex-array/src/expr/vtable.rs index e15d539944c..f61b068d57b 100644 --- a/vortex-array/src/expr/vtable.rs +++ b/vortex-array/src/expr/vtable.rs @@ -15,11 +15,17 @@ use vortex_dtype::DType; use vortex_error::VortexExpect; use vortex_error::VortexResult; use vortex_error::vortex_bail; -use vortex_mask::Mask; -use vortex_vector::Datum; +use vortex_error::vortex_ensure; +use vortex_scalar::Scalar; use vortex_vector::VectorOps; use crate::ArrayRef; +use crate::Canonical; +use crate::Executable; +use crate::ExecutionCtx; +use crate::IntoArray; +use crate::arrays::ConstantArray; +use crate::arrays::ConstantVTable; use crate::expr::ExprId; use crate::expr::StatsCatalog; use crate::expr::expression::Expression; @@ -96,11 +102,11 @@ pub trait VTable: 'static + Sized + Send + Sync { /// Execute the expression on the given vector with the given dtype. /// /// This function will become required in a future release. - fn execute(&self, options: &Self::Options, args: ExecutionArgs) -> VortexResult { - _ = options; - drop(args); - vortex_bail!("Expression {} does not support execution", self.id()); - } + fn execute( + &self, + options: &Self::Options, + args: ExecutionArgs, + ) -> VortexResult; /// Implement an abstract reduction rule over a tree of scalar functions. /// @@ -305,23 +311,59 @@ pub trait SimplifyCtx { } /// Arguments for expression execution. -pub struct ExecutionArgs { - /// The input datums for the expression, one per child. - pub datums: Vec, - /// The input dtypes for the expression, one per child. - pub dtypes: Vec, +pub struct ExecutionArgs<'a> { + /// The inputs for the expression, one per child. + pub inputs: Vec, /// The row count of the execution scope. pub row_count: usize, - /// The expected return dtype of the expression, as computed by [`Expression::return_dtype`]. - pub return_dtype: DType, + /// The execution context. + pub ctx: &'a mut ExecutionCtx, } -/// Arguments for expression validity execution. -pub struct ValidityExecutionArgs { - /// The input masks for the expression, one per child. - pub inputs: Vec, - /// The row count of the execution scope. - pub row_count: usize, +/// The result of expression execution. +pub enum ExecutionResult { + Array(Canonical), + Scalar(ConstantArray), +} + +impl ExecutionResult { + pub fn constant>(scalar: S, len: usize) -> Self { + ExecutionResult::Scalar(ConstantArray::new(scalar.into(), len)) + } + + /// Returns the length of this execution result. + pub fn len(&self) -> usize { + match self { + ExecutionResult::Array(canonical) => canonical.len(), + ExecutionResult::Scalar(constant) => constant.len(), + } + } + + /// Returns the data type of this execution result. + pub fn dtype(&self) -> &DType { + match self { + ExecutionResult::Array(canonical) => canonical.dtype(), + ExecutionResult::Scalar(constant) => constant.dtype(), + } + } +} + +impl Executable for ExecutionResult { + fn execute(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult { + Ok(match array.as_opt::() { + None => ExecutionResult::Array(array.execute::(ctx)?), + Some(constant) => ExecutionResult::Scalar(constant.clone()), + }) + } +} + +impl IntoArray for ExecutionResult { + fn into_array(self) -> ArrayRef { + match self { + ExecutionResult::Array(canonical) => canonical.into_array(), + ExecutionResult::Scalar(constant) => constant.into_array(), + } + } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -391,7 +433,7 @@ pub trait DynExprVTable: 'static + Send + Sync + private::Sealed { ) -> VortexResult>; fn simplify_untyped(&self, expression: &Expression) -> VortexResult>; fn validity(&self, expression: &Expression) -> VortexResult>; - fn execute(&self, options: &dyn Any, args: ExecutionArgs) -> VortexResult; + fn execute(&self, options: &dyn Any, args: ExecutionArgs) -> VortexResult; fn evaluate(&self, expression: &Expression, scope: &ArrayRef) -> VortexResult; fn reduce( &self, @@ -504,38 +546,41 @@ impl DynExprVTable for VTableAdapter { ) } - fn execute(&self, options: &dyn Any, args: ExecutionArgs) -> VortexResult { + fn execute(&self, options: &dyn Any, args: ExecutionArgs) -> VortexResult { let options = downcast::(options); let expected_row_count = args.row_count; #[cfg(debug_assertions)] - let expected_dtype = args.return_dtype.clone(); + let expected_dtype = { + let args_dtypes: Vec = args + .inputs + .iter() + .map(|array| array.dtype().clone()) + .collect(); + V::return_dtype(&self.0, options, &args_dtypes) + }?; let result = V::execute(&self.0, options, args)?; - if let Datum::Vector(v) = &result { - assert_eq!( - v.len(), - expected_row_count, - "Expression execution {} returned vector of length {}, but expected {}", - self.0.id(), - v.len(), - expected_row_count, - ); - } + assert_eq!( + result.len(), + expected_row_count, + "Expression execution {} returned vector of length {}, but expected {}", + self.0.id(), + result.len(), + expected_row_count, + ); // In debug mode, validate that the output dtype matches the expected return dtype. #[cfg(debug_assertions)] { - use vortex_vector::datum_matches_dtype; - - if !datum_matches_dtype(&result, &expected_dtype) { - vortex_bail!( - "Expression execution returned datum of invalid dtype. Expected {}, got {:?}", - expected_dtype, - result - ); - } + vortex_ensure!( + result.dtype() == &expected_dtype, + "Expression execution {} returned vector of invalid dtype. Expected {}, got {}", + self.0.id(), + expected_dtype, + result.dtype(), + ); } Ok(result)