Skip to content

Commit 898a14e

Browse files
committed
list_transform: use rebuild_with_referenced_valid_elements, drop the Lists enum
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent be09d8d commit 898a14e

2 files changed

Lines changed: 73 additions & 180 deletions

File tree

‎vortex-array/src/scalar_fn/fns/list_transform/mod.rs‎

Lines changed: 55 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,10 @@ use std::fmt::Formatter;
1010
use std::sync::Arc;
1111

1212
use itertools::Itertools;
13-
use num_traits::AsPrimitive;
14-
use vortex_buffer::Buffer;
15-
use vortex_buffer::BufferMut;
1613
use vortex_error::VortexResult;
1714
use vortex_error::vortex_bail;
1815
use vortex_error::vortex_ensure;
1916
use vortex_error::vortex_ensure_eq;
20-
use vortex_mask::AllOr;
2117
use vortex_session::registry::CachedId;
2218

2319
use crate::ArrayRef;
@@ -26,25 +22,20 @@ use crate::IntoArray;
2622
use crate::arrays::List;
2723
use crate::arrays::ListArray;
2824
use crate::arrays::ListViewArray;
29-
use crate::arrays::PrimitiveArray;
3025
use crate::arrays::list::ListArrayExt;
3126
use crate::arrays::list::ListArraySlotsExt;
3227
use crate::arrays::listview::ListViewArrayExt;
3328
use crate::arrays::listview::ListViewArraySlotsExt;
34-
use crate::arrays::listview::ListViewRebuildMode;
35-
use crate::arrays::listview::list_from_list_view;
3629
use crate::dtype::DType;
3730
use crate::dtype::FieldName;
3831
use crate::expr::BoundLambda;
3932
use crate::expr::Lambda;
4033
use crate::expr::display::ExprDisplay;
41-
use crate::match_each_integer_ptype;
4234
use crate::scalar_fn::Arity;
4335
use crate::scalar_fn::ChildName;
4436
use crate::scalar_fn::ExecutionArgs;
4537
use crate::scalar_fn::ScalarFnId;
4638
use crate::scalar_fn::ScalarFnVTable;
47-
use crate::validity::Validity;
4839

4940
/// Apply a lambda to every element of each list, keeping each list's length and validity.
5041
///
@@ -140,12 +131,10 @@ impl ScalarFnVTable for ListTransform {
140131
let ListTransformOptions::Bound(lambda) = options else {
141132
vortex_bail!("list_transform()'s lambda is not bound");
142133
};
143-
let list = if lambda.is_infallible() {
144-
Lists::as_they_are(args.get(0)?, ctx)?
145-
} else {
146-
Lists::visible_elements_only(args.get(0)?, ctx)?
147-
};
148-
list.with_elements(|elements| lambda.evaluate(vec![elements.clone()], elements.len()))
134+
match args.get(0)?.try_downcast::<List>() {
135+
Ok(list) => transform_list(list, lambda, ctx),
136+
Err(list) => transform_list_view(list.execute::<ListViewArray>(ctx)?, lambda, ctx),
137+
}
149138
}
150139

151140
fn is_strict(&self, _options: &ListTransformOptions) -> bool {
@@ -185,167 +174,62 @@ fn element_dtype(list: &DType, params: &[FieldName]) -> VortexResult<[DType; 1]>
185174
Ok([element.as_ref().clone()])
186175
}
187176

188-
/// The lists the lambda runs over: their elements buffer, and the offsets (and sizes) that put the
189-
/// results back into lists of the same shape.
190-
enum Lists {
191-
List(ListArray),
192-
View(ListViewArray),
193-
}
194-
195-
impl Lists {
196-
/// The lists as they are. For a lambda that can't fail: it runs over the whole elements buffer,
197-
/// and results for elements no valid list shows are never read.
198-
fn as_they_are(list: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
199-
Ok(match list.try_downcast::<List>() {
200-
Ok(list) => Self::List(list),
201-
Err(list) => Self::View(list.execute::<ListViewArray>(ctx)?),
202-
})
203-
}
204-
205-
/// The lists with nothing in their elements buffer but the elements of valid lists. For a lambda
206-
/// that can fail, which must not fail on a value nobody sees.
207-
///
208-
/// The lists keep their shape, trimmed of what comes before the first list and after the last,
209-
/// unless a null list holds elements or a list view's lists aren't laid out back to back. Only
210-
/// then are the elements copied.
211-
fn visible_elements_only(list: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
212-
let list = match list.try_downcast::<List>() {
213-
Ok(list) => match trim_list(&list, ctx)? {
214-
Some(trimmed) => return Ok(Self::List(trimmed)),
215-
None => list.into_array(),
216-
},
217-
Err(list) => list,
218-
};
219-
let view = list.execute::<ListViewArray>(ctx)?;
220-
match empty_null_lists(&view, ctx)? {
221-
None if view.is_zero_copy_to_list() => Ok(Self::View(
222-
view.rebuild(ListViewRebuildMode::TrimElements, ctx)?,
223-
)),
224-
None => Ok(Self::List(list_from_list_view(view, ctx)?)),
225-
Some(emptied) => Ok(Self::List(list_from_list_view(emptied, ctx)?)),
226-
}
227-
}
228-
229-
/// The same lists, with `f` of their elements buffer as their elements.
230-
fn with_elements(
231-
self,
232-
f: impl FnOnce(&ArrayRef) -> VortexResult<ArrayRef>,
233-
) -> VortexResult<ArrayRef> {
234-
match self {
235-
Self::List(list) => {
236-
let values = f(list.elements())?;
237-
vortex_ensure_eq!(values.len(), list.elements().len());
238-
// SAFETY: the offsets and validity are the list's, over one value per element.
239-
Ok(unsafe {
240-
ListArray::new_unchecked(values, list.offsets().clone(), list.list_validity())
241-
}
242-
.into_array())
243-
}
244-
Self::View(view) => {
245-
let values = f(view.elements())?;
246-
vortex_ensure_eq!(values.len(), view.elements().len());
247-
// SAFETY: the offsets, sizes and validity are the view's, over one value per
248-
// element, so the views keep their bounds and their layout.
249-
Ok(unsafe {
250-
ListViewArray::new_unchecked(
251-
values,
252-
view.offsets().clone(),
253-
view.sizes().clone(),
254-
view.listview_validity(),
255-
)
256-
.with_zero_copy_to_list(view.is_zero_copy_to_list())
257-
}
258-
.into_array())
259-
}
260-
}
261-
}
262-
}
263-
264-
/// The list without what comes before its first list and after its last, or `None` when a null
265-
/// list holds elements, which only a rebuild can drop.
266-
fn trim_list(list: &ListArray, ctx: &mut ExecutionCtx) -> VortexResult<Option<ListArray>> {
267-
let offsets = list.offsets().clone().execute::<PrimitiveArray>(ctx)?;
268-
let nulls = null_runs(&list.list_validity(), list.len(), ctx)?;
269-
match_each_integer_ptype!(offsets.ptype(), |O| {
270-
let offsets = offsets.as_slice::<O>();
271-
// The offsets are sorted, so a run of null lists is empty when it starts and ends at the
272-
// same offset.
273-
if nulls
274-
.iter()
275-
.any(|&(start, end)| offsets[start] != offsets[end])
276-
{
277-
return Ok(None);
278-
}
279-
let first = offsets[0];
280-
let (start, end): (usize, usize) = (first.as_(), offsets[offsets.len() - 1].as_());
281-
if start == 0 && end == list.elements().len() {
282-
return Ok(Some(list.clone()));
283-
}
284-
let shifted: Buffer<O> = offsets.iter().map(|&offset| offset - first).collect();
285-
// SAFETY: the elements are cut to the lists' range and the offsets shifted by its start,
286-
// so every list keeps its elements.
287-
Ok(Some(unsafe {
288-
ListArray::new_unchecked(
289-
list.elements().slice(start..end)?,
290-
shifted.into_array(),
291-
list.list_validity(),
292-
)
293-
}))
294-
})
177+
/// The lambda over a list's elements, inside the list's offsets and validity.
178+
///
179+
/// A lambda that can fail runs only on the elements valid lists reference. One that can't runs on
180+
/// every element from the first list to the last: results under null lists are never read.
181+
fn transform_list(
182+
list: ListArray,
183+
lambda: &BoundLambda,
184+
ctx: &mut ExecutionCtx,
185+
) -> VortexResult<ArrayRef> {
186+
let list = if lambda.is_infallible() {
187+
trim_ends(list, ctx)?
188+
} else {
189+
list.rebuild_with_referenced_valid_elements(ctx)?
190+
};
191+
let values = lambda.evaluate(vec![list.elements().clone()], list.elements().len())?;
192+
vortex_ensure_eq!(values.len(), list.elements().len());
193+
// SAFETY: the offsets and validity are the list's, over one value per element.
194+
Ok(
195+
unsafe { ListArray::new_unchecked(values, list.offsets().clone(), list.list_validity()) }
196+
.into_array(),
197+
)
295198
}
296199

297-
/// The runs of null rows, as `(start, end)` ranges.
298-
fn null_runs(
299-
validity: &Validity,
300-
len: usize,
200+
/// The lambda over a list view's elements, inside the view's offsets, sizes and validity.
201+
///
202+
/// A lambda that can fail runs only on the elements valid lists reference.
203+
fn transform_list_view(
204+
view: ListViewArray,
205+
lambda: &BoundLambda,
301206
ctx: &mut ExecutionCtx,
302-
) -> VortexResult<Vec<(usize, usize)>> {
303-
if matches!(validity, Validity::NonNullable | Validity::AllValid) {
304-
return Ok(Vec::new());
207+
) -> VortexResult<ArrayRef> {
208+
let view = if lambda.is_infallible() {
209+
view
210+
} else {
211+
view.rebuild_with_referenced_valid_elements(ctx)?
212+
};
213+
let values = lambda.evaluate(vec![view.elements().clone()], view.elements().len())?;
214+
vortex_ensure_eq!(values.len(), view.elements().len());
215+
// SAFETY: the offsets, sizes and validity are the view's, over one value per element, so the
216+
// views keep their bounds and their layout.
217+
Ok(unsafe {
218+
ListViewArray::new_unchecked(
219+
values,
220+
view.offsets().clone(),
221+
view.sizes().clone(),
222+
view.listview_validity(),
223+
)
224+
.with_zero_copy_to_list(view.is_zero_copy_to_list())
305225
}
306-
Ok(match (!&validity.execute_mask(len, ctx)?).slices() {
307-
AllOr::All => vec![(0, len)],
308-
AllOr::None => Vec::new(),
309-
AllOr::Some(runs) => runs.to_vec(),
310-
})
226+
.into_array())
311227
}
312228

313-
/// The lists with every null list emptied, so that nothing under a null list reaches the lambda, or
314-
/// `None` when no null list holds elements.
315-
fn empty_null_lists(
316-
list: &ListViewArray,
317-
ctx: &mut ExecutionCtx,
318-
) -> VortexResult<Option<ListViewArray>> {
319-
let validity = list.listview_validity();
320-
let nulls = null_runs(&validity, list.len(), ctx)?;
321-
if nulls.is_empty() {
322-
return Ok(None);
229+
/// The list without what comes before its first list and after its last.
230+
fn trim_ends(list: ListArray, ctx: &mut ExecutionCtx) -> VortexResult<ListArray> {
231+
if list.offset_at(0, ctx)? == 0 && list.offset_at(list.len(), ctx)? == list.elements().len() {
232+
return Ok(list);
323233
}
324-
let sizes = list.sizes().clone().execute::<PrimitiveArray>(ctx)?;
325-
let sizes = match_each_integer_ptype!(sizes.ptype(), |T| {
326-
let sizes = sizes.as_slice::<T>();
327-
// Writers usually leave null lists empty already.
328-
if nulls
329-
.iter()
330-
.all(|&(start, end)| sizes[start..end].iter().all(|size| *size == T::default()))
331-
{
332-
return Ok(None);
333-
}
334-
let mut sizes = BufferMut::copy_from(sizes);
335-
for &(start, end) in &nulls {
336-
sizes[start..end].fill(T::default());
337-
}
338-
PrimitiveArray::new(sizes.freeze(), Validity::NonNullable)
339-
});
340-
// SAFETY: only sizes changed, and only to 0, so every view stays within the elements. The new
341-
// array is not marked zero-copy to a list: an emptied list leaves its elements behind as a gap,
342-
// which the rebuild must drop.
343-
Ok(Some(unsafe {
344-
ListViewArray::new_unchecked(
345-
list.elements().clone(),
346-
list.offsets().clone(),
347-
sizes.into_array(),
348-
validity,
349-
)
350-
}))
234+
list.reset_offsets(false, ctx)
351235
}

‎vortex-array/src/scalar_fn/fns/list_transform/tests.rs‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ use std::sync::LazyLock;
77
use rstest::rstest;
88
use vortex_buffer::buffer;
99
use vortex_error::VortexResult;
10+
use vortex_error::vortex_err;
1011
use vortex_session::VortexSession;
1112

1213
use super::ListTransform;
1314
use super::ListTransformOptions;
14-
use super::Lists;
15+
use super::transform_list;
1516
use crate::ArrayRef;
1617
use crate::IntoArray;
1718
use crate::RecursiveCanonical;
@@ -20,10 +21,12 @@ use crate::arrays::BoolArray;
2021
use crate::arrays::Dict;
2122
use crate::arrays::DictArray;
2223
use crate::arrays::FixedSizeListArray;
24+
use crate::arrays::List;
2325
use crate::arrays::ListArray;
2426
use crate::arrays::ListViewArray;
2527
use crate::arrays::PrimitiveArray;
2628
use crate::arrays::StructArray;
29+
use crate::arrays::list::ListArraySlotsExt;
2730
use crate::arrays::listview::ListViewArraySlotsExt;
2831
use crate::assert_arrays_eq;
2932
use crate::dtype::DType;
@@ -203,15 +206,21 @@ fn a_lambda_that_can_fail_sees_only_the_lists_elements(
203206
assert_applies(input()?, &list_transform(root(), plus_one()?), expected)
204207
}
205208

206-
#[test]
207-
fn a_lambda_that_can_fail_runs_on_a_slice_without_its_leftovers() -> VortexResult<()> {
208-
// Canonicalizing the result drops the leftovers too, so look at what the lambda runs on.
209+
#[rstest]
210+
#[case::cannot_fail(eq(var("x"), lit(3_i32)))]
211+
#[case::can_fail(checked_add(var("x"), lit(1_i32)))]
212+
fn the_lambda_runs_on_a_slice_without_its_leftovers(#[case] body: Expression) -> VortexResult<()> {
213+
// Canonicalizing the result drops the leftovers too, so look at the transformed list itself.
209214
let mut ctx = SESSION.create_execution_ctx();
210-
let lists = Lists::visible_elements_only(sliced_list_with_leftovers()?, &mut ctx)?;
211-
lists.with_elements(|elements| {
212-
assert_arrays_eq!(elements.clone(), buffer![1_i32, 2].into_array(), &mut ctx);
213-
Ok(elements.clone())
214-
})?;
215+
let lambda = Lambda::new(["x"], body)?
216+
.bind(&[DType::Primitive(PType::I32, Nullability::NonNullable)])?;
217+
let list = sliced_list_with_leftovers()?
218+
.try_downcast::<List>()
219+
.map_err(|_| vortex_err!("not a list"))?;
220+
let result = transform_list(list, &lambda, &mut ctx)?
221+
.try_downcast::<List>()
222+
.map_err(|_| vortex_err!("not a list"))?;
223+
assert_eq!(result.elements().len(), 2);
215224
Ok(())
216225
}
217226

0 commit comments

Comments
 (0)