Skip to main content

polars_core/series/arithmetic/
list.rs

1//! Allow arithmetic operations for ListChunked.
2//! use polars_error::{feature_gated, PolarsResult};
3
4use polars_error::{PolarsResult, feature_gated};
5
6use super::list_utils::NumericOp;
7use super::{IntoSeries, ListChunked, ListType, NumOpsDispatchInner, Series};
8use crate::prelude::DataType;
9
10impl NumOpsDispatchInner for ListType {
11    fn add_to(lhs: &ListChunked, rhs: &Series) -> PolarsResult<Series> {
12        NumericListOp::add().execute(&lhs.clone().into_series(), rhs)
13    }
14
15    fn subtract(lhs: &ListChunked, rhs: &Series) -> PolarsResult<Series> {
16        NumericListOp::sub().execute(&lhs.clone().into_series(), rhs)
17    }
18
19    fn multiply(lhs: &ListChunked, rhs: &Series) -> PolarsResult<Series> {
20        NumericListOp::mul().execute(&lhs.clone().into_series(), rhs)
21    }
22
23    fn divide(lhs: &ListChunked, rhs: &Series) -> PolarsResult<Series> {
24        NumericListOp::div().execute(&lhs.clone().into_series(), rhs)
25    }
26
27    fn remainder(lhs: &ListChunked, rhs: &Series) -> PolarsResult<Series> {
28        NumericListOp::rem().execute(&lhs.clone().into_series(), rhs)
29    }
30}
31
32#[cfg_attr(not(feature = "list_arithmetic"), allow(unused))]
33#[derive(Clone)]
34pub struct NumericListOp(NumericOp);
35
36impl NumericListOp {
37    pub fn add() -> Self {
38        Self(NumericOp::Add)
39    }
40
41    pub fn sub() -> Self {
42        Self(NumericOp::Sub)
43    }
44
45    pub fn mul() -> Self {
46        Self(NumericOp::Mul)
47    }
48
49    pub fn div() -> Self {
50        Self(NumericOp::Div)
51    }
52
53    pub fn rem() -> Self {
54        Self(NumericOp::Rem)
55    }
56
57    pub fn floor_div() -> Self {
58        Self(NumericOp::FloorDiv)
59    }
60
61    pub fn trunc_div() -> Self {
62        Self(NumericOp::TruncDiv)
63    }
64
65    pub fn try_get_leaf_supertype(
66        &self,
67        prim_dtype_lhs: &DataType,
68        prim_dtype_rhs: &DataType,
69    ) -> PolarsResult<DataType> {
70        self.0
71            .try_get_leaf_supertype(prim_dtype_lhs, prim_dtype_rhs)
72    }
73}
74
75impl NumericListOp {
76    #[cfg_attr(not(feature = "list_arithmetic"), allow(unused))]
77    pub fn execute(&self, lhs: &Series, rhs: &Series) -> PolarsResult<Series> {
78        feature_gated!("list_arithmetic", {
79            use std::borrow::Cow;
80
81            use either::Either;
82
83            // `trim_to_normalized_offsets` ensures we don't perform excessive
84            // memory allocation / compute on memory regions that have been
85            // sliced out.
86            let lhs = lhs
87                .trim_lists_to_normalized_offsets()
88                .map_or(Cow::Borrowed(lhs), Cow::Owned);
89            let rhs = rhs
90                .trim_lists_to_normalized_offsets()
91                .map_or(Cow::Borrowed(rhs), Cow::Owned);
92
93            let lhs = lhs.rechunk();
94            let rhs = rhs.rechunk();
95
96            let binary_op_exec = match ListNumericOpHelper::try_new(
97                self.clone(),
98                lhs.name().clone(),
99                lhs.dtype(),
100                rhs.dtype(),
101                lhs.len(),
102                rhs.len(),
103                {
104                    let (a, b) = lhs.list_offsets_and_validities_recursive();
105                    debug_assert!(a.iter().all(|x| *x.first() as usize == 0));
106                    (a, b, lhs.clone())
107                },
108                {
109                    let (a, b) = rhs.list_offsets_and_validities_recursive();
110                    debug_assert!(a.iter().all(|x| *x.first() as usize == 0));
111                    (a, b, rhs.clone())
112                },
113                lhs.rechunk_validity(),
114                rhs.rechunk_validity(),
115            )? {
116                Either::Left(v) => v,
117                Either::Right(ca) => return Ok(ca.into_series()),
118            };
119
120            Ok(binary_op_exec.finish()?.into_series())
121        })
122    }
123}
124
125#[cfg(feature = "list_arithmetic")]
126use inner::ListNumericOpHelper;
127
128#[cfg(feature = "list_arithmetic")]
129mod inner {
130    use either::Either;
131    use list_utils::with_match_pl_num_arith;
132    use num_traits::Zero;
133    use polars_arrow::bitmap::Bitmap;
134    use polars_arrow::compute::utils::combine_validities_and;
135    use polars_arrow::offset::OffsetsBuffer;
136    use polars_compute::arithmetic::pl_num::PlNumArithmetic;
137    use polars_utils::float::IsFloat;
138
139    use super::super::list_utils::{BinaryOpApplyType, Broadcast, NumericOp};
140    use super::super::*;
141
142    /// Utility to perform a binary operation between the primitive values of
143    /// 2 columns, where at least one of the columns is a `ListChunked` type.
144    pub(super) struct ListNumericOpHelper {
145        op: NumericListOp,
146        output_name: PlSmallStr,
147        op_apply_type: BinaryOpApplyType,
148        broadcast: Broadcast,
149        output_dtype: DataType,
150        output_primitive_dtype: DataType,
151        output_len: usize,
152        /// Outer validity of the result, we always materialize this to reduce the
153        /// amount of code paths we need.
154        outer_validity: Bitmap,
155        // The series are stored as they are used for list broadcasting.
156        data_lhs: (Vec<OffsetsBuffer<i64>>, Vec<Option<Bitmap>>, Series),
157        data_rhs: (Vec<OffsetsBuffer<i64>>, Vec<Option<Bitmap>>, Series),
158        list_to_prim_lhs: Option<(Box<dyn Array>, usize)>,
159        swapped: bool,
160    }
161
162    /// This lets us separate some logic into `new()` to reduce the amount of
163    /// monomorphized code.
164    impl ListNumericOpHelper {
165        /// Checks that:
166        /// * Dtypes are compatible:
167        ///   * list<->primitive | primitive<->list
168        ///   * list<->list both contain primitives (e.g. List<Int8>)
169        /// * Primitive dtypes match
170        /// * Lengths are compatible:
171        ///   * 1<->n | n<->1
172        ///   * n<->n
173        /// * Both sides have at least 1 non-NULL outer row.
174        ///
175        /// Does not check:
176        /// * Whether the offsets are aligned for list<->list, this will be checked during execution.
177        ///
178        /// This returns an `Either` which may contain the final result to simplify
179        /// the implementation.
180        #[allow(clippy::too_many_arguments)]
181        pub(super) fn try_new(
182            op: NumericListOp,
183            output_name: PlSmallStr,
184            dtype_lhs: &DataType,
185            dtype_rhs: &DataType,
186            len_lhs: usize,
187            len_rhs: usize,
188            data_lhs: (Vec<OffsetsBuffer<i64>>, Vec<Option<Bitmap>>, Series),
189            data_rhs: (Vec<OffsetsBuffer<i64>>, Vec<Option<Bitmap>>, Series),
190            validity_lhs: Option<Bitmap>,
191            validity_rhs: Option<Bitmap>,
192        ) -> PolarsResult<Either<Self, ListChunked>> {
193            let prim_dtype_lhs = dtype_lhs.leaf_dtype();
194            let prim_dtype_rhs = dtype_rhs.leaf_dtype();
195
196            let output_primitive_dtype =
197                op.0.try_get_leaf_supertype(prim_dtype_lhs, prim_dtype_rhs)?;
198
199            fn is_list_type_at_all_levels(dtype: &DataType) -> bool {
200                match dtype {
201                    DataType::List(inner) => is_list_type_at_all_levels(inner),
202                    dt if dt.is_supported_list_arithmetic_input() => true,
203                    _ => false,
204                }
205            }
206
207            let op_err_msg = |err_reason: &str| {
208                polars_err!(
209                    InvalidOperation:
210                    "cannot {} columns: {}: (left: {}, right: {})",
211                    op.0.name(), err_reason, dtype_lhs, dtype_rhs,
212                )
213            };
214
215            let ensure_list_type_at_all_levels = |dtype: &DataType| {
216                if !is_list_type_at_all_levels(dtype) {
217                    Err(op_err_msg("dtype was not list on all nesting levels"))
218                } else {
219                    Ok(())
220                }
221            };
222
223            let (op_apply_type, output_dtype) = match (dtype_lhs, dtype_rhs) {
224                (l @ DataType::List(a), r @ DataType::List(b)) => {
225                    // `get_arithmetic_field()` in the DSL checks this, but we also have to check here because if a user
226                    // directly adds 2 series together it bypasses the DSL.
227                    // This is currently duplicated code and should be replaced one day with an assert after Series ops get
228                    // checked properly.
229                    if ![a, b]
230                        .into_iter()
231                        .all(|x| x.is_supported_list_arithmetic_input())
232                    {
233                        polars_bail!(
234                            InvalidOperation:
235                            "cannot {} two list columns with non-numeric inner types: (left: {}, right: {})",
236                            op.0.name(), l, r,
237                        );
238                    }
239                    (BinaryOpApplyType::ListToList, l)
240                },
241                (list_dtype @ DataType::List(_), x) if x.is_supported_list_arithmetic_input() => {
242                    ensure_list_type_at_all_levels(list_dtype)?;
243                    (BinaryOpApplyType::ListToPrimitive, list_dtype)
244                },
245                (x, list_dtype @ DataType::List(_)) if x.is_supported_list_arithmetic_input() => {
246                    ensure_list_type_at_all_levels(list_dtype)?;
247                    (BinaryOpApplyType::PrimitiveToList, list_dtype)
248                },
249                (l, r) => polars_bail!(
250                    InvalidOperation:
251                    "{} operation not supported for dtypes: {} != {}",
252                    op.0.name(), l, r,
253                ),
254            };
255
256            let output_dtype = output_dtype.cast_leaf(output_primitive_dtype.clone());
257
258            let (broadcast, output_len) = match (len_lhs, len_rhs) {
259                (l, r) if l == r => (Broadcast::NoBroadcast, l),
260                (1, v) => (Broadcast::Left, v),
261                (v, 1) => (Broadcast::Right, v),
262                (l, r) => polars_bail!(
263                    ShapeMismatch:
264                    "cannot {} two columns of differing lengths: {} != {}",
265                    op.0.name(), l, r
266                ),
267            };
268
269            let DataType::List(output_inner_dtype) = &output_dtype else {
270                unreachable!()
271            };
272
273            // # NULL semantics
274            // * [[1, 2]] (List[List[Int64]]) + NULL (Int64) => [[NULL, NULL]]
275            //   * Essentially as if the NULL primitive was added to every primitive in the row of the list column.
276            // * NULL (List[Int64]) + 1   (Int64)       => NULL
277            // * NULL (List[Int64]) + [1] (List[Int64]) => NULL
278
279            if output_len == 0
280                || (matches!(
281                    &op_apply_type,
282                    BinaryOpApplyType::ListToList | BinaryOpApplyType::ListToPrimitive
283                ) && validity_lhs.as_ref().is_some_and(|x| x.set_bits() == 0))
284                || (matches!(
285                    &op_apply_type,
286                    BinaryOpApplyType::ListToList | BinaryOpApplyType::PrimitiveToList
287                ) && validity_rhs.as_ref().is_some_and(|x| x.set_bits() == 0))
288            {
289                return Ok(Either::Right(ListChunked::full_null_with_dtype(
290                    output_name,
291                    output_len,
292                    output_inner_dtype.as_ref(),
293                )));
294            }
295
296            // At this point:
297            // * All unit length list columns have a valid outer value.
298
299            // The outer validity is just the validity of any non-broadcasting lists.
300            let outer_validity = match (&op_apply_type, &broadcast, validity_lhs, validity_rhs) {
301                // Both lists with same length, we combine the validity.
302                (BinaryOpApplyType::ListToList, Broadcast::NoBroadcast, l, r) => {
303                    combine_validities_and(l.as_ref(), r.as_ref())
304                },
305                // Match all other combinations that have non-broadcasting lists.
306                (
307                    BinaryOpApplyType::ListToList | BinaryOpApplyType::ListToPrimitive,
308                    Broadcast::NoBroadcast | Broadcast::Right,
309                    v,
310                    _,
311                )
312                | (
313                    BinaryOpApplyType::ListToList | BinaryOpApplyType::PrimitiveToList,
314                    Broadcast::NoBroadcast | Broadcast::Left,
315                    _,
316                    v,
317                ) => v,
318                _ => None,
319            }
320            .unwrap_or_else(|| Bitmap::new_with_value(true, output_len));
321
322            Ok(Either::Left(Self {
323                op,
324                output_name,
325                op_apply_type,
326                broadcast,
327                output_dtype: output_dtype.clone(),
328                output_primitive_dtype,
329                output_len,
330                outer_validity,
331                data_lhs,
332                data_rhs,
333                list_to_prim_lhs: None,
334                swapped: false,
335            }))
336        }
337
338        pub(super) fn finish(mut self) -> PolarsResult<ListChunked> {
339            // We have physical codepaths for a subset of the possible combinations of broadcasting and
340            // column types. The remaining combinations are handled by dispatching to the physical
341            // codepaths after operand swapping and/or materialized broadcasting.
342            //
343            // # Physical impl table
344            // Legend
345            // * |  N  | // impl "N"
346            // * | [N] | // dispatches to impl "N"
347            //
348            //                  |  L  |  N  |  R  | // Broadcast (L)eft, (N)oBroadcast, (R)ight
349            // ListToList       | [1] |  0  |  1  |
350            // ListToPrimitive  | [2] |  2  |  3  | // list broadcasting just materializes and dispatches to NoBroadcast
351            // PrimitiveToList  | [3] | [2] | [2] |
352
353            self.swapped = true;
354
355            match (&self.op_apply_type, &self.broadcast) {
356                (BinaryOpApplyType::ListToList, Broadcast::NoBroadcast)
357                | (BinaryOpApplyType::ListToList, Broadcast::Right)
358                | (BinaryOpApplyType::ListToPrimitive, Broadcast::NoBroadcast)
359                | (BinaryOpApplyType::ListToPrimitive, Broadcast::Right) => {
360                    self.swapped = false;
361                    self._finish_impl_dispatch()
362                },
363                (BinaryOpApplyType::ListToList, Broadcast::Left) => {
364                    self.broadcast = Broadcast::Right;
365
366                    std::mem::swap(&mut self.data_lhs, &mut self.data_rhs);
367                    self._finish_impl_dispatch()
368                },
369                (BinaryOpApplyType::ListToPrimitive, Broadcast::Left) => {
370                    self.list_to_prim_lhs
371                        .replace(Self::materialize_broadcasted_list(
372                            &mut self.data_lhs,
373                            self.output_len,
374                            &self.output_primitive_dtype,
375                        ));
376
377                    self.broadcast = Broadcast::NoBroadcast;
378
379                    // This does not swap! We are just dispatching to `NoBroadcast`
380                    // after materializing the broadcasted list array.
381                    self.swapped = false;
382                    self._finish_impl_dispatch()
383                },
384                (BinaryOpApplyType::PrimitiveToList, Broadcast::NoBroadcast) => {
385                    self.op_apply_type = BinaryOpApplyType::ListToPrimitive;
386
387                    std::mem::swap(&mut self.data_lhs, &mut self.data_rhs);
388                    self._finish_impl_dispatch()
389                },
390                (BinaryOpApplyType::PrimitiveToList, Broadcast::Right) => {
391                    // We materialize the list columns with `new_from_index`, as otherwise we'd have to
392                    // implement logic that broadcasts the offsets and validities across multiple levels
393                    // of nesting. But we will re-use the materialized memory to store the result.
394
395                    self.list_to_prim_lhs
396                        .replace(Self::materialize_broadcasted_list(
397                            &mut self.data_rhs,
398                            self.output_len,
399                            &self.output_primitive_dtype,
400                        ));
401
402                    self.op_apply_type = BinaryOpApplyType::ListToPrimitive;
403                    self.broadcast = Broadcast::NoBroadcast;
404
405                    std::mem::swap(&mut self.data_lhs, &mut self.data_rhs);
406                    self._finish_impl_dispatch()
407                },
408                (BinaryOpApplyType::PrimitiveToList, Broadcast::Left) => {
409                    self.op_apply_type = BinaryOpApplyType::ListToPrimitive;
410                    self.broadcast = Broadcast::Right;
411
412                    std::mem::swap(&mut self.data_lhs, &mut self.data_rhs);
413                    self._finish_impl_dispatch()
414                },
415            }
416        }
417
418        fn _finish_impl_dispatch(&mut self) -> PolarsResult<ListChunked> {
419            let output_dtype = self.output_dtype.clone();
420            let output_len = self.output_len;
421
422            let prim_lhs = self
423                .data_lhs
424                .2
425                .get_leaf_array()
426                .cast(&self.output_primitive_dtype)?
427                .rechunk();
428            let prim_rhs = self
429                .data_rhs
430                .2
431                .get_leaf_array()
432                .cast(&self.output_primitive_dtype)?
433                .rechunk();
434
435            debug_assert_eq!(prim_lhs.dtype(), prim_rhs.dtype());
436            let prim_dtype = prim_lhs.dtype();
437            debug_assert_eq!(prim_dtype, &self.output_primitive_dtype);
438
439            // Safety: Leaf dtypes have been checked to be numeric by `try_new()`
440            let out = with_match_physical_numeric_polars_type!(&prim_dtype, |$T| {
441                self._finish_impl::<$T>(prim_lhs, prim_rhs)
442            })?;
443
444            debug_assert_eq!(out.dtype(), &output_dtype);
445            assert_eq!(out.len(), output_len);
446
447            Ok(out)
448        }
449
450        /// Internal use only - contains physical impls.
451        fn _finish_impl<T: PolarsNumericType>(
452            &mut self,
453            prim_s_lhs: Series,
454            prim_s_rhs: Series,
455        ) -> PolarsResult<ListChunked>
456        where
457            T::Native: PlNumArithmetic,
458            PrimitiveArray<T::Native>:
459                polars_compute::comparisons::TotalEqKernel<Scalar = T::Native>,
460            T::Native: Zero + IsFloat,
461        {
462            #[inline(never)]
463            fn check_mismatch_pos(
464                mismatch_pos: usize,
465                offsets_lhs: &OffsetsBuffer<i64>,
466                offsets_rhs: &OffsetsBuffer<i64>,
467            ) -> PolarsResult<()> {
468                if mismatch_pos < offsets_lhs.len_proxy() {
469                    // RHS could be broadcasted
470                    let len_r = offsets_rhs.length_at(if offsets_rhs.len_proxy() == 1 {
471                        0
472                    } else {
473                        mismatch_pos
474                    });
475                    polars_bail!(
476                        ShapeMismatch:
477                        "list lengths differed at index {}: {} != {}",
478                        mismatch_pos,
479                        offsets_lhs.length_at(mismatch_pos), len_r
480                    )
481                }
482                Ok(())
483            }
484
485            let mut arr_lhs = {
486                let ca: &ChunkedArray<T> = prim_s_lhs.as_ref().as_ref();
487                assert_eq!(ca.chunks().len(), 1);
488                ca.downcast_get(0).unwrap().clone()
489            };
490
491            let mut arr_rhs = {
492                let ca: &ChunkedArray<T> = prim_s_rhs.as_ref().as_ref();
493                assert_eq!(ca.chunks().len(), 1);
494                ca.downcast_get(0).unwrap().clone()
495            };
496
497            match (&self.op_apply_type, &self.broadcast) {
498                // We skip for this because it dispatches to `ArithmeticKernel`, which handles the
499                // validities for us.
500                (BinaryOpApplyType::ListToPrimitive, Broadcast::Right) => {},
501                _ if self.list_to_prim_lhs.is_none() => {
502                    self.op.0.prepare_numeric_op_side_validities::<T>(
503                        &mut arr_lhs,
504                        &mut arr_rhs,
505                        self.swapped,
506                    )
507                },
508                (BinaryOpApplyType::ListToPrimitive, Broadcast::NoBroadcast) => {
509                    // `self.list_to_prim_lhs` is `Some(_)`, this is handled later.
510                },
511                _ => unreachable!(),
512            }
513
514            //
515            // General notes
516            // * Lists can be:
517            //   * Sliced, in which case the primitive/leaf array needs to be indexed starting from an
518            //     offset instead of 0.
519            //   * Masked, in which case the masked rows are permitted to have non-matching widths.
520            //
521
522            let out = match (&self.op_apply_type, &self.broadcast) {
523                (BinaryOpApplyType::ListToList, Broadcast::NoBroadcast) => {
524                    let offsets_lhs = &self.data_lhs.0[0];
525                    let offsets_rhs = &self.data_rhs.0[0];
526
527                    assert_eq!(offsets_lhs.len_proxy(), offsets_rhs.len_proxy());
528
529                    // Output primitive (and optional validity) are aligned to the LHS input.
530                    let n_values = arr_lhs.len();
531                    let mut out_vec: Vec<T::Native> = Vec::with_capacity(n_values);
532                    let out_ptr: *mut T::Native = out_vec.as_mut_ptr();
533
534                    // Counter that stops being incremented at the first row position with mismatching
535                    // list lengths.
536                    let mut mismatch_pos = 0;
537
538                    with_match_pl_num_arith!(&self.op.0, self.swapped, |$OP| {
539                        for (i, ((lhs_start, lhs_len), (rhs_start, rhs_len))) in offsets_lhs
540                            .offset_and_length_iter()
541                            .zip(offsets_rhs.offset_and_length_iter())
542                            .enumerate()
543                        {
544                            if
545                                (mismatch_pos == i)
546                                & (
547                                    (lhs_len == rhs_len)
548                                    | unsafe { !self.outer_validity.get_bit_unchecked(i) }
549                                )
550                            {
551                                mismatch_pos += 1;
552                            }
553
554                            // Both sides are lists, we restrict the index to the min length to avoid
555                            // OOB memory access.
556                            let len: usize = lhs_len.min(rhs_len);
557
558                            for i in 0..len {
559                                let l_idx = i + lhs_start;
560                                let r_idx = i + rhs_start;
561
562                                let l = unsafe { arr_lhs.value_unchecked(l_idx) };
563                                let r = unsafe { arr_rhs.value_unchecked(r_idx) };
564                                let v = $OP(l, r);
565
566                                unsafe { out_ptr.add(l_idx).write(v) };
567                            }
568                        }
569                    });
570
571                    check_mismatch_pos(mismatch_pos, offsets_lhs, offsets_rhs)?;
572
573                    unsafe { out_vec.set_len(n_values) };
574
575                    /// Reduce monomorphization
576                    #[inline(never)]
577                    fn combine_validities_list_to_list_no_broadcast(
578                        offsets_lhs: &OffsetsBuffer<i64>,
579                        offsets_rhs: &OffsetsBuffer<i64>,
580                        validity_lhs: Option<&Bitmap>,
581                        validity_rhs: Option<&Bitmap>,
582                        len_lhs: usize,
583                    ) -> Option<Bitmap> {
584                        match (validity_lhs, validity_rhs) {
585                            (Some(l), Some(r)) => Some((l.clone().make_mut(), r)),
586                            (Some(v), None) => return Some(v.clone()),
587                            (None, Some(v)) => {
588                                Some((Bitmap::new_with_value(true, len_lhs).make_mut(), v))
589                            },
590                            (None, None) => None,
591                        }
592                        .map(|(mut validity_out, validity_rhs)| {
593                            for ((lhs_start, lhs_len), (rhs_start, rhs_len)) in offsets_lhs
594                                .offset_and_length_iter()
595                                .zip(offsets_rhs.offset_and_length_iter())
596                            {
597                                let len: usize = lhs_len.min(rhs_len);
598
599                                for i in 0..len {
600                                    let l_idx = i + lhs_start;
601                                    let r_idx = i + rhs_start;
602
603                                    let l_valid = unsafe { validity_out.get_unchecked(l_idx) };
604                                    let r_valid = unsafe { validity_rhs.get_bit_unchecked(r_idx) };
605                                    let is_valid = l_valid & r_valid;
606
607                                    // Size and alignment of validity vec are based on LHS.
608                                    unsafe { validity_out.set_unchecked(l_idx, is_valid) };
609                                }
610                            }
611
612                            validity_out.freeze()
613                        })
614                    }
615
616                    let leaf_validity = combine_validities_list_to_list_no_broadcast(
617                        offsets_lhs,
618                        offsets_rhs,
619                        arr_lhs.validity(),
620                        arr_rhs.validity(),
621                        arr_lhs.len(),
622                    );
623
624                    let arr =
625                        PrimitiveArray::<T::Native>::from_vec(out_vec).with_validity(leaf_validity);
626
627                    let (offsets, validities, _) = std::mem::take(&mut self.data_lhs);
628                    assert_eq!(offsets.len(), 1);
629
630                    self.finish_offsets_and_validities(Box::new(arr), offsets, validities)
631                },
632                (BinaryOpApplyType::ListToList, Broadcast::Right) => {
633                    let offsets_lhs = &self.data_lhs.0[0];
634                    let offsets_rhs = &self.data_rhs.0[0];
635
636                    // Output primitive (and optional validity) are aligned to the LHS input.
637                    let n_values = arr_lhs.len();
638                    let mut out_vec: Vec<T::Native> = Vec::with_capacity(n_values);
639                    let out_ptr: *mut T::Native = out_vec.as_mut_ptr();
640
641                    assert_eq!(offsets_rhs.len_proxy(), 1);
642                    let rhs_start = *offsets_rhs.first() as usize;
643                    let width = offsets_rhs.range() as usize;
644
645                    let mut mismatch_pos = 0;
646
647                    with_match_pl_num_arith!(&self.op.0, self.swapped, |$OP| {
648                        for (i, (lhs_start, lhs_len)) in offsets_lhs.offset_and_length_iter().enumerate() {
649                            if ((lhs_len == width) & (mismatch_pos == i))
650                                | unsafe { !self.outer_validity.get_bit_unchecked(i) }
651                            {
652                                mismatch_pos += 1;
653                            }
654
655                            let len: usize = lhs_len.min(width);
656
657                            for i in 0..len {
658                                let l_idx = i + lhs_start;
659                                let r_idx = i + rhs_start;
660
661                                let l = unsafe { arr_lhs.value_unchecked(l_idx) };
662                                let r = unsafe { arr_rhs.value_unchecked(r_idx) };
663                                let v = $OP(l, r);
664
665                                unsafe {
666                                    out_ptr.add(l_idx).write(v);
667                                }
668                            }
669                        }
670                    });
671
672                    check_mismatch_pos(mismatch_pos, offsets_lhs, offsets_rhs)?;
673
674                    unsafe { out_vec.set_len(n_values) };
675
676                    #[inline(never)]
677                    fn combine_validities_list_to_list_broadcast_right(
678                        offsets_lhs: &OffsetsBuffer<i64>,
679                        validity_lhs: Option<&Bitmap>,
680                        validity_rhs: Option<&Bitmap>,
681                        len_lhs: usize,
682                        width: usize,
683                        rhs_start: usize,
684                    ) -> Option<Bitmap> {
685                        match (validity_lhs, validity_rhs) {
686                            (Some(l), Some(r)) => Some((l.clone().make_mut(), r)),
687                            (Some(v), None) => return Some(v.clone()),
688                            (None, Some(v)) => {
689                                Some((Bitmap::new_with_value(true, len_lhs).make_mut(), v))
690                            },
691                            (None, None) => None,
692                        }
693                        .map(|(mut validity_out, validity_rhs)| {
694                            for (lhs_start, lhs_len) in offsets_lhs.offset_and_length_iter() {
695                                let len: usize = lhs_len.min(width);
696
697                                for i in 0..len {
698                                    let l_idx = i + lhs_start;
699                                    let r_idx = i + rhs_start;
700
701                                    let l_valid = unsafe { validity_out.get_unchecked(l_idx) };
702                                    let r_valid = unsafe { validity_rhs.get_bit_unchecked(r_idx) };
703                                    let is_valid = l_valid & r_valid;
704
705                                    // Size and alignment of validity vec are based on LHS.
706                                    unsafe { validity_out.set_unchecked(l_idx, is_valid) };
707                                }
708                            }
709
710                            validity_out.freeze()
711                        })
712                    }
713
714                    let leaf_validity = combine_validities_list_to_list_broadcast_right(
715                        offsets_lhs,
716                        arr_lhs.validity(),
717                        arr_rhs.validity(),
718                        arr_lhs.len(),
719                        width,
720                        rhs_start,
721                    );
722
723                    let arr =
724                        PrimitiveArray::<T::Native>::from_vec(out_vec).with_validity(leaf_validity);
725
726                    let (offsets, validities, _) = std::mem::take(&mut self.data_lhs);
727                    assert_eq!(offsets.len(), 1);
728
729                    self.finish_offsets_and_validities(Box::new(arr), offsets, validities)
730                },
731                (BinaryOpApplyType::ListToPrimitive, Broadcast::NoBroadcast)
732                    if self.list_to_prim_lhs.is_none() =>
733                {
734                    let offsets_lhs = self.data_lhs.0.as_slice();
735
736                    // Notes
737                    // * Primitive indexing starts from 0
738                    // * Output is aligned to LHS array
739
740                    let n_values = arr_lhs.len();
741                    let mut out_vec = Vec::<T::Native>::with_capacity(n_values);
742                    let out_ptr = out_vec.as_mut_ptr();
743
744                    with_match_pl_num_arith!(&self.op.0, self.swapped, |$OP| {
745                        for (i, l_range) in OffsetsBuffer::<i64>::leaf_ranges_iter(offsets_lhs).enumerate()
746                        {
747                            let r = unsafe { arr_rhs.value_unchecked(i) };
748                            for l_idx in l_range {
749                                unsafe {
750                                    let l = arr_lhs.value_unchecked(l_idx);
751                                    let v = $OP(l, r);
752                                    out_ptr.add(l_idx).write(v);
753                                }
754                            }
755                        }
756                    });
757
758                    unsafe { out_vec.set_len(n_values) }
759
760                    let leaf_validity = combine_validities_list_to_primitive_no_broadcast(
761                        offsets_lhs,
762                        arr_lhs.validity(),
763                        arr_rhs.validity(),
764                        arr_lhs.len(),
765                    );
766
767                    let arr =
768                        PrimitiveArray::<T::Native>::from_vec(out_vec).with_validity(leaf_validity);
769
770                    let (offsets, validities, _) = std::mem::take(&mut self.data_lhs);
771                    self.finish_offsets_and_validities(Box::new(arr), offsets, validities)
772                },
773                // If we are dispatched here, it means that the LHS array is a unique allocation created
774                // after a unit-length list column was broadcasted, so this codepath mutably stores the
775                // results back into the LHS array to save memory.
776                (BinaryOpApplyType::ListToPrimitive, Broadcast::NoBroadcast) => {
777                    let offsets_lhs = self.data_lhs.0.as_slice();
778
779                    let (mut arr, n_values) = Option::take(&mut self.list_to_prim_lhs).unwrap();
780                    let arr = arr
781                        .as_any_mut()
782                        .downcast_mut::<PrimitiveArray<T::Native>>()
783                        .unwrap();
784                    let mut arr_lhs = std::mem::take(arr);
785
786                    self.op.0.prepare_numeric_op_side_validities::<T>(
787                        &mut arr_lhs,
788                        &mut arr_rhs,
789                        self.swapped,
790                    );
791
792                    let arr_lhs_mut_slice = arr_lhs.get_mut_values().unwrap();
793                    assert_eq!(arr_lhs_mut_slice.len(), n_values);
794
795                    with_match_pl_num_arith!(&self.op.0, self.swapped, |$OP| {
796                        for (i, l_range) in OffsetsBuffer::<i64>::leaf_ranges_iter(offsets_lhs).enumerate()
797                        {
798                            let r = unsafe { arr_rhs.value_unchecked(i) };
799                            for l_idx in l_range {
800                                unsafe {
801                                    let l = arr_lhs_mut_slice.get_unchecked_mut(l_idx);
802                                    *l = $OP(*l, r);
803                                }
804                            }
805                        }
806                    });
807
808                    let leaf_validity = combine_validities_list_to_primitive_no_broadcast(
809                        offsets_lhs,
810                        arr_lhs.validity(),
811                        arr_rhs.validity(),
812                        arr_lhs.len(),
813                    );
814
815                    let arr = arr_lhs.with_validity(leaf_validity);
816
817                    let (offsets, validities, _) = std::mem::take(&mut self.data_lhs);
818                    self.finish_offsets_and_validities(Box::new(arr), offsets, validities)
819                },
820                (BinaryOpApplyType::ListToPrimitive, Broadcast::Right) => {
821                    assert_eq!(arr_rhs.len(), 1);
822
823                    let Some(r) = (unsafe { arr_rhs.get_unchecked(0) }) else {
824                        // RHS is single primitive NULL, create the result by setting the leaf validity to all-NULL.
825                        let (offsets, validities, _) = std::mem::take(&mut self.data_lhs);
826                        return Ok(self.finish_offsets_and_validities(
827                            Box::new(
828                                arr_lhs.clone().with_validity(Some(Bitmap::new_with_value(
829                                    false,
830                                    arr_lhs.len(),
831                                ))),
832                            ),
833                            offsets,
834                            validities,
835                        ));
836                    };
837
838                    let arr = self
839                        .op
840                        .0
841                        .apply_array_to_scalar::<T>(arr_lhs, r, self.swapped);
842                    let (offsets, validities, _) = std::mem::take(&mut self.data_lhs);
843
844                    self.finish_offsets_and_validities(Box::new(arr), offsets, validities)
845                },
846                v @ (BinaryOpApplyType::PrimitiveToList, Broadcast::Right)
847                | v @ (BinaryOpApplyType::ListToList, Broadcast::Left)
848                | v @ (BinaryOpApplyType::ListToPrimitive, Broadcast::Left)
849                | v @ (BinaryOpApplyType::PrimitiveToList, Broadcast::Left)
850                | v @ (BinaryOpApplyType::PrimitiveToList, Broadcast::NoBroadcast) => {
851                    if cfg!(debug_assertions) {
852                        panic!("operation was not re-written: {v:?}")
853                    } else {
854                        unreachable!()
855                    }
856                },
857            };
858
859            Ok(out)
860        }
861
862        /// Construct the result `ListChunked` from the leaf array and the offsets/validities of every
863        /// level.
864        fn finish_offsets_and_validities(
865            &mut self,
866            leaf_array: Box<dyn Array>,
867            offsets: Vec<OffsetsBuffer<i64>>,
868            validities: Vec<Option<Bitmap>>,
869        ) -> ListChunked {
870            assert!(!offsets.is_empty());
871            assert_eq!(offsets.len(), validities.len());
872            let mut results = leaf_array;
873
874            let mut iter = offsets.into_iter().zip(validities).rev();
875
876            while iter.len() > 1 {
877                let (offsets, validity) = iter.next().unwrap();
878                let dtype = LargeListArray::default_datatype(results.dtype().clone());
879                results = Box::new(LargeListArray::new(dtype, offsets, results, validity));
880            }
881
882            // The combined outer validity is pre-computed during `try_new()`
883            let (offsets, _) = iter.next().unwrap();
884            let validity = std::mem::take(&mut self.outer_validity);
885            let dtype = LargeListArray::default_datatype(results.dtype().clone());
886            let results = LargeListArray::new(dtype, offsets, results, Some(validity));
887
888            ListChunked::with_chunk(std::mem::take(&mut self.output_name), results)
889        }
890
891        fn materialize_broadcasted_list(
892            side_data: &mut (Vec<OffsetsBuffer<i64>>, Vec<Option<Bitmap>>, Series),
893            output_len: usize,
894            output_primitive_dtype: &DataType,
895        ) -> (Box<dyn Array>, usize) {
896            let s = &side_data.2;
897            assert_eq!(s.len(), 1);
898
899            let expected_n_values = {
900                let offsets = s.list_offsets_and_validities_recursive().0;
901                output_len * OffsetsBuffer::<i64>::leaf_full_start_end(&offsets).len()
902            };
903
904            let ca = s.list().unwrap();
905            // Remember to cast the leaf primitives to the supertype.
906            let ca = ca
907                .cast(&ca.dtype().cast_leaf(output_primitive_dtype.clone()))
908                .unwrap();
909            assert!(output_len > 1); // In case there is a fast-path that doesn't give us owned data.
910            let ca = ca.new_from_index(0, output_len).rechunk();
911
912            let s = ca.into_series();
913
914            *side_data = {
915                let (a, b) = s.list_offsets_and_validities_recursive();
916                // `Series::default()`: This field in the tuple is no longer used.
917                (a, b, Series::default())
918            };
919
920            let n_values = OffsetsBuffer::<i64>::leaf_full_start_end(&side_data.0).len();
921            assert_eq!(n_values, expected_n_values);
922
923            let mut s = s.get_leaf_array();
924            let v = unsafe { s.chunks_mut() };
925
926            assert_eq!(v.len(), 1);
927            (v.swap_remove(0), n_values)
928        }
929    }
930
931    /// Used in 2 places, so it's outside here.
932    #[inline(never)]
933    fn combine_validities_list_to_primitive_no_broadcast(
934        offsets_lhs: &[OffsetsBuffer<i64>],
935        validity_lhs: Option<&Bitmap>,
936        validity_rhs: Option<&Bitmap>,
937        len_lhs: usize,
938    ) -> Option<Bitmap> {
939        match (validity_lhs, validity_rhs) {
940            (Some(l), Some(r)) => Some((l.clone().make_mut(), r)),
941            (Some(v), None) => return Some(v.clone()),
942            // Materialize a full-true validity to re-use the codepath, as we still
943            // need to spread the bits from the RHS to the correct positions.
944            (None, Some(v)) => Some((Bitmap::new_with_value(true, len_lhs).make_mut(), v)),
945            (None, None) => None,
946        }
947        .map(|(mut validity_out, validity_rhs)| {
948            for (i, l_range) in OffsetsBuffer::<i64>::leaf_ranges_iter(offsets_lhs).enumerate() {
949                let r_valid = unsafe { validity_rhs.get_bit_unchecked(i) };
950                for l_idx in l_range {
951                    let l_valid = unsafe { validity_out.get_unchecked(l_idx) };
952                    let is_valid = l_valid & r_valid;
953
954                    // Size and alignment of validity vec are based on LHS.
955                    unsafe { validity_out.set_unchecked(l_idx, is_valid) };
956                }
957            }
958
959            validity_out.freeze()
960        })
961    }
962}