Skip to content

Commit cd55cf5

Browse files
committed
keep the lists' shape unless a lambda that can fail would see hidden elements
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 33a0e28 commit cd55cf5

2 files changed

Lines changed: 202 additions & 37 deletions

File tree

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

Lines changed: 120 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::sync::Arc;
1111

1212
use itertools::Itertools;
1313
use num_traits::AsPrimitive;
14+
use vortex_buffer::Buffer;
1415
use vortex_buffer::BufferMut;
1516
use vortex_error::VortexResult;
1617
use vortex_error::vortex_bail;
@@ -30,6 +31,7 @@ use crate::arrays::list::ListArrayExt;
3031
use crate::arrays::list::ListArraySlotsExt;
3132
use crate::arrays::listview::ListViewArrayExt;
3233
use crate::arrays::listview::ListViewArraySlotsExt;
34+
use crate::arrays::listview::ListViewRebuildMode;
3335
use crate::arrays::listview::list_from_list_view;
3436
use crate::dtype::DType;
3537
use crate::dtype::FieldName;
@@ -47,7 +49,8 @@ use crate::validity::Validity;
4749
/// Apply a lambda to every element of each list, keeping each list's length and validity.
4850
///
4951
/// The lambda takes one parameter, the element. The only child is the list, of `List` dtype in any
50-
/// encoding, and the result is a `List`. The lambda runs only on the elements of valid lists.
52+
/// encoding, and the result is a `List` of the same shape. A lambda that can fail runs only on the
53+
/// elements of valid lists.
5154
#[derive(Clone)]
5255
pub struct ListTransform;
5356

@@ -137,15 +140,12 @@ impl ScalarFnVTable for ListTransform {
137140
let ListTransformOptions::Bound(lambda) = options else {
138141
vortex_bail!("list_transform()'s lambda is not bound");
139142
};
140-
let list = exact_lists(args.get(0)?, ctx)?;
141-
let elements = list.elements();
142-
let values = lambda.evaluate(vec![elements.clone()], elements.len())?;
143-
vortex_ensure_eq!(values.len(), elements.len());
144-
// SAFETY: the offsets are the list's, over one value per element.
145-
Ok(unsafe {
146-
ListArray::new_unchecked(values, list.offsets().clone(), list.list_validity())
147-
}
148-
.into_array())
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()))
149149
}
150150

151151
fn is_strict(&self, _options: &ListTransformOptions) -> bool {
@@ -185,34 +185,113 @@ fn element_dtype(list: &DType, params: &[FieldName]) -> VortexResult<[DType; 1]>
185185
Ok([element.as_ref().clone()])
186186
}
187187

188-
/// The lists laid out so that every element belongs to exactly one valid list, in order: nothing
189-
/// comes before the first list or after the last, and a null list has no elements.
190-
fn exact_lists(list: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<ListArray> {
191-
// A list array usually is exact already, and then needs no rebuild.
192-
let list = match list.try_downcast::<List>() {
193-
Ok(list) if is_exact(&list, ctx)? => return Ok(list),
194-
Ok(list) => list.into_array(),
195-
Err(list) => list,
196-
};
197-
let list = list.execute::<ListViewArray>(ctx)?;
198-
list_from_list_view(empty_null_lists(list, ctx)?, ctx)
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),
199193
}
200194

201-
/// Whether nothing comes before the first list or after the last, and no null list holds elements.
202-
fn is_exact(list: &ListArray, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
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>> {
203267
let offsets = list.offsets().clone().execute::<PrimitiveArray>(ctx)?;
204268
let nulls = null_runs(&list.list_validity(), list.len(), ctx)?;
205-
Ok(match_each_integer_ptype!(offsets.ptype(), |O| {
269+
match_each_integer_ptype!(offsets.ptype(), |O| {
206270
let offsets = offsets.as_slice::<O>();
207-
let last: usize = offsets[offsets.len() - 1].as_();
208271
// The offsets are sorted, so a run of null lists is empty when it starts and ends at the
209272
// same offset.
210-
offsets[0] == O::default()
211-
&& last == list.elements().len()
212-
&& nulls
213-
.iter()
214-
.all(|&(start, end)| offsets[start] == offsets[end])
215-
}))
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+
})
216295
}
217296

218297
/// The runs of null rows, as `(start, end)` ranges.
@@ -231,12 +310,16 @@ fn null_runs(
231310
})
232311
}
233312

234-
/// The lists with every null list emptied, so that nothing under a null list reaches the lambda.
235-
fn empty_null_lists(list: ListViewArray, ctx: &mut ExecutionCtx) -> VortexResult<ListViewArray> {
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>> {
236319
let validity = list.listview_validity();
237320
let nulls = null_runs(&validity, list.len(), ctx)?;
238321
if nulls.is_empty() {
239-
return Ok(list);
322+
return Ok(None);
240323
}
241324
let sizes = list.sizes().clone().execute::<PrimitiveArray>(ctx)?;
242325
let sizes = match_each_integer_ptype!(sizes.ptype(), |T| {
@@ -246,7 +329,7 @@ fn empty_null_lists(list: ListViewArray, ctx: &mut ExecutionCtx) -> VortexResult
246329
.iter()
247330
.all(|&(start, end)| sizes[start..end].iter().all(|size| *size == T::default()))
248331
{
249-
return Ok(list);
332+
return Ok(None);
250333
}
251334
let mut sizes = BufferMut::copy_from(sizes);
252335
for &(start, end) in &nulls {
@@ -257,12 +340,12 @@ fn empty_null_lists(list: ListViewArray, ctx: &mut ExecutionCtx) -> VortexResult
257340
// SAFETY: only sizes changed, and only to 0, so every view stays within the elements. The new
258341
// array is not marked zero-copy to a list: an emptied list leaves its elements behind as a gap,
259342
// which the rebuild must drop.
260-
Ok(unsafe {
343+
Ok(Some(unsafe {
261344
ListViewArray::new_unchecked(
262345
list.elements().clone(),
263346
list.offsets().clone(),
264347
sizes.into_array(),
265348
validity,
266349
)
267-
})
350+
}))
268351
}

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
use std::sync::Arc;
55

6+
use rstest::rstest;
67
use vortex_buffer::buffer;
78
use vortex_error::VortexResult;
89

910
use super::ListTransform;
1011
use super::ListTransformOptions;
12+
use super::Lists;
1113
use crate::ArrayRef;
1214
use crate::IntoArray;
1315
use crate::RecursiveCanonical;
@@ -21,6 +23,7 @@ use crate::arrays::ListArray;
2123
use crate::arrays::ListViewArray;
2224
use crate::arrays::PrimitiveArray;
2325
use crate::arrays::StructArray;
26+
use crate::arrays::listview::ListViewArraySlotsExt;
2427
use crate::assert_arrays_eq;
2528
use crate::dtype::DType;
2629
use crate::dtype::FieldNames;
@@ -146,6 +149,85 @@ fn a_sliced_list_skips_a_null_lists_elements() -> VortexResult<()> {
146149
assert_applies(lists, &list_transform(root(), plus_one()?), expected)
147150
}
148151

152+
/// `[[1], [2]]` over `[MAX, 1, 2, MAX]`.
153+
fn sliced_list_with_leftovers() -> VortexResult<ArrayRef> {
154+
list_of(
155+
buffer![i32::MAX, 1_i32, 2, i32::MAX].into_array(),
156+
&[1, 2, 3],
157+
Validity::NonNullable,
158+
)
159+
}
160+
161+
/// `[[1], [2]]` over `[MAX, 1, 2]`: the lists are back to back, after a leftover.
162+
fn list_view_with_a_leftover_in_front() -> VortexResult<ArrayRef> {
163+
let mut ctx = array_session().create_execution_ctx();
164+
let view = ListViewArray::try_new(
165+
buffer![i32::MAX, 1_i32, 2].into_array(),
166+
buffer![1_u32, 2].into_array(),
167+
buffer![1_u32, 1].into_array(),
168+
Validity::NonNullable,
169+
&mut ctx,
170+
)?;
171+
// SAFETY: the lists are sorted, with nothing between them.
172+
Ok(unsafe { view.with_zero_copy_to_list(true) }.into_array())
173+
}
174+
175+
/// `[[1], [2]]` over `[1, MAX, 2]`: the lists skip an element.
176+
fn list_view_with_a_gap() -> VortexResult<ArrayRef> {
177+
let mut ctx = array_session().create_execution_ctx();
178+
Ok(ListViewArray::try_new(
179+
buffer![1_i32, i32::MAX, 2].into_array(),
180+
buffer![0_u32, 2].into_array(),
181+
buffer![1_u32, 1].into_array(),
182+
Validity::NonNullable,
183+
&mut ctx,
184+
)?
185+
.into_array())
186+
}
187+
188+
/// `x + 1` would overflow on `i32::MAX`, which no list shows.
189+
#[rstest]
190+
#[case::sliced_list(sliced_list_with_leftovers)]
191+
#[case::list_view_with_a_leftover_in_front(list_view_with_a_leftover_in_front)]
192+
#[case::list_view_with_a_gap(list_view_with_a_gap)]
193+
fn a_lambda_that_can_fail_sees_only_the_lists_elements(
194+
#[case] input: fn() -> VortexResult<ArrayRef>,
195+
) -> VortexResult<()> {
196+
let expected = list_of(
197+
buffer![2_i32, 3].into_array(),
198+
&[0, 1, 2],
199+
Validity::NonNullable,
200+
)?;
201+
assert_applies(input()?, &list_transform(root(), plus_one()?), expected)
202+
}
203+
204+
#[test]
205+
fn a_lambda_that_can_fail_runs_on_a_slice_without_its_leftovers() -> VortexResult<()> {
206+
// Canonicalizing the result drops the leftovers too, so look at what the lambda runs on.
207+
let mut ctx = array_session().create_execution_ctx();
208+
let lists = Lists::visible_elements_only(sliced_list_with_leftovers()?, &mut ctx)?;
209+
lists.with_elements(|elements| {
210+
assert_arrays_eq!(elements.clone(), buffer![1_i32, 2].into_array(), &mut ctx);
211+
Ok(elements.clone())
212+
})?;
213+
Ok(())
214+
}
215+
216+
#[test]
217+
fn a_lambda_that_cannot_fail_runs_on_the_elements_in_place() -> VortexResult<()> {
218+
// `x = 3` cannot fail, so it runs over the whole elements buffer, the null list's `i32::MAX`
219+
// included, and nothing is copied.
220+
let mut ctx = array_session().create_execution_ctx();
221+
let result = lists()?
222+
.apply(&list_transform(
223+
root(),
224+
Lambda::new(["x"], eq(var("x"), lit(3_i32)))?,
225+
))?
226+
.execute::<ListViewArray>(&mut ctx)?;
227+
assert_eq!(result.elements().len(), 5);
228+
Ok(())
229+
}
230+
149231
#[test]
150232
fn every_list_null() -> VortexResult<()> {
151233
// `[null (hiding [i32::MAX]), null (hiding [i32::MAX])]`

0 commit comments

Comments
 (0)