Skip to content

Commit b9493f8

Browse files
committed
list_transform scalar fn
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent b9e059b commit b9493f8

8 files changed

Lines changed: 1259 additions & 0 deletions

File tree

‎vortex-array/src/expr/exprs.rs‎

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

44
//! Factory functions for creating [`Expression`]s from scalar function vtables.
55
6+
use std::iter;
67
use std::sync::Arc;
78

89
use vortex_error::VortexExpect;
10+
use vortex_error::VortexResult;
911
use vortex_error::vortex_panic;
1012
use vortex_utils::iter::ReduceBalancedIterExt;
1113

@@ -16,6 +18,7 @@ use crate::dtype::FieldNames;
1618
use crate::dtype::Nullability;
1719
use crate::expr::BoundExpression;
1820
use crate::expr::Expression;
21+
use crate::expr::Lambda;
1922
use crate::scalar::Scalar;
2023
use crate::scalar::ScalarValue;
2124
use crate::scalar_fn::EmptyOptions;
@@ -42,6 +45,7 @@ use crate::scalar_fn::fns::list_contains::ListContains;
4245
use crate::scalar_fn::fns::list_contains::ListContainsOptions;
4346
use crate::scalar_fn::fns::list_length::ListLength;
4447
use crate::scalar_fn::fns::list_sum::ListSum;
48+
use crate::scalar_fn::fns::list_transform::ListTransform;
4549
use crate::scalar_fn::fns::literal::Literal;
4650
use crate::scalar_fn::fns::mask::Mask;
4751
use crate::scalar_fn::fns::merge::DuplicateHandling;
@@ -1284,6 +1288,32 @@ pub fn bound_list_sum_opts(
12841288
.vortex_expect("list-sum expressions require a numeric list child")
12851289
}
12861290

1291+
// ---- ListTransform ----
1292+
1293+
/// Creates an expression that applies a lambda to every element of each list.
1294+
///
1295+
/// The lambda runs in an inner domain of one row per element, and takes `params`: the element,
1296+
/// then optionally the element's zero-based `u64` index within its list. Its `body` reads them
1297+
/// with [`var`], and may read the outer domain too, for example with [`col`]. See [`Lambda::new`]
1298+
/// for how the body is rewritten. Returns an error when a parameter name repeats.
1299+
///
1300+
/// ```rust
1301+
/// # use vortex_array::expr::{checked_add, col, list_transform, var};
1302+
/// let expr = list_transform(col("l"), ["x"], checked_add(var("x"), col("a"))).unwrap();
1303+
/// assert_eq!(
1304+
/// expr.to_string(),
1305+
/// "vortex.list.transform($.l, $.a, (x, c0) -> ($.x + $.c0))"
1306+
/// );
1307+
/// ```
1308+
pub fn list_transform(
1309+
list: Expression,
1310+
params: impl IntoIterator<Item = impl Into<FieldName>>,
1311+
body: Expression,
1312+
) -> VortexResult<Expression> {
1313+
let (lambda, captures) = Lambda::new(params, body)?;
1314+
ListTransform.try_new_expr(lambda, iter::once(list).chain(captures))
1315+
}
1316+
12871317
/// Reinterpret timestamp wall times in `options.time_zone`, preserving the time unit.
12881318
///
12891319
/// `ambiguous` is a UTF-8 expression containing `raise`, `earliest`, `latest`, or `null`.

‎vortex-array/src/expr/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub use exprs::list_contains_opts;
9191
pub use exprs::list_length;
9292
pub use exprs::list_sum;
9393
pub use exprs::list_sum_opts;
94+
pub use exprs::list_transform;
9495
pub use exprs::lit;
9596
pub use exprs::lt;
9697
pub use exprs::lt_eq;
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use num_traits::AsPrimitive;
5+
use vortex_buffer::BitBufferMut;
6+
use vortex_buffer::BufferMut;
7+
use vortex_error::VortexResult;
8+
use vortex_error::vortex_ensure;
9+
use vortex_mask::Mask;
10+
11+
use crate::ArrayRef;
12+
use crate::Canonical;
13+
use crate::ExecutionCtx;
14+
use crate::IntoArray;
15+
use crate::arrays::ConstantArray;
16+
use crate::arrays::FixedSizeListArray;
17+
use crate::arrays::List;
18+
use crate::arrays::ListArray;
19+
use crate::arrays::ListViewArray;
20+
use crate::arrays::PrimitiveArray;
21+
use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
22+
use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
23+
use crate::arrays::list::ListArrayExt;
24+
use crate::arrays::list::ListArraySlotsExt;
25+
use crate::arrays::listview::ListViewArrayExt;
26+
use crate::arrays::listview::ListViewArraySlotsExt;
27+
use crate::dtype::DType;
28+
use crate::dtype::NativePType;
29+
use crate::match_each_integer_ptype;
30+
use crate::validity::Validity;
31+
32+
/// The elements a per-element lambda runs over, list after list, and how to rebuild the lists
33+
/// around one result per element.
34+
///
35+
/// Every element of a non-null list is included. A null list's elements are dropped when they must
36+
/// not reach the lambda, for a body that can fail; otherwise they stay, and their results stay
37+
/// hidden behind the list's validity. Nothing is copied unless the included ranges are not already
38+
/// laid out back to back.
39+
pub(super) struct ListElements {
40+
elements: ArrayRef,
41+
/// Where each list's elements start, then where the last one's end: `n + 1` offsets from 0.
42+
offsets: ArrayRef,
43+
validity: Validity,
44+
}
45+
46+
impl ListElements {
47+
pub(super) fn new(
48+
list: ArrayRef,
49+
keep_null_lists: bool,
50+
ctx: &mut ExecutionCtx,
51+
) -> VortexResult<Self> {
52+
// A list array is used as it is, unless a null list holds elements that must not reach
53+
// the lambda. Writers usually leave null lists empty.
54+
if let Some(list) = list.as_opt::<List>()
55+
&& list.offset_at(0)? == 0
56+
{
57+
let validity = list.list_validity();
58+
if keep_null_lists || !null_lists_hold_elements(list.offsets(), &validity, ctx)? {
59+
return Ok(Self {
60+
elements: list.sliced_elements()?,
61+
offsets: list.offsets().clone(),
62+
validity,
63+
});
64+
}
65+
}
66+
67+
let (elements, starts, mut sizes, validity) = list_parts(list, ctx)?;
68+
if !keep_null_lists && !matches!(validity, Validity::NonNullable | Validity::AllValid) {
69+
let valid = validity.execute_mask(sizes.len(), ctx)?;
70+
for (list, size) in sizes.iter_mut().enumerate() {
71+
if !valid.value(list) {
72+
*size = 0;
73+
}
74+
}
75+
}
76+
let elements = gather(elements, &starts, &sizes)?;
77+
let mut offsets = BufferMut::<u64>::with_capacity(sizes.len() + 1);
78+
let mut end = 0;
79+
offsets.push(end);
80+
for size in sizes {
81+
end += size;
82+
offsets.push(end);
83+
}
84+
Ok(Self {
85+
elements,
86+
offsets: offsets.into_array(),
87+
validity,
88+
})
89+
}
90+
91+
/// The number of elements each list contributes, in list order.
92+
fn sizes(&self, ctx: &mut ExecutionCtx) -> VortexResult<Vec<u64>> {
93+
let offsets = to_u64s(&self.offsets, ctx)?;
94+
Ok(offsets
95+
.windows(2)
96+
.map(|bound| bound[1] - bound[0])
97+
.collect())
98+
}
99+
100+
/// The number of elements.
101+
pub(super) fn len(&self) -> usize {
102+
self.elements.len()
103+
}
104+
105+
/// The elements, list after list.
106+
pub(super) fn elements(&self) -> &ArrayRef {
107+
&self.elements
108+
}
109+
110+
/// Each element's position within its list: sizes `[2, 0, 3]` give `[0, 1, 0, 1, 2]`.
111+
pub(super) fn indices(&self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
112+
// A list's positions are written in whole blocks, `0..BLOCK` then `BLOCK..2 * BLOCK` and so
113+
// on, and the last block may spill past the list's end: the next list overwrites the spill,
114+
// and the buffer has room for the final one. A list no longer than a block then costs one
115+
// store and no branch on its length, which a position-by-position loop mispredicts at the
116+
// end of most lists.
117+
const BLOCK: usize = 16;
118+
let len = self.len();
119+
let mut indices = BufferMut::<u64>::with_capacity(len + BLOCK);
120+
let out = indices.as_mut_ptr();
121+
let first: [u64; BLOCK] = std::array::from_fn(|i| i as u64);
122+
let mut start = 0;
123+
for size in self.sizes(ctx)? {
124+
let end = start + usize::try_from(size)?;
125+
assert!(end <= len, "list sizes add up to more than {len} elements");
126+
let (mut at, mut block) = (start, first);
127+
// An empty list writes a block too, which the next list overwrites.
128+
loop {
129+
// SAFETY: `at` is `start` or below `end`, so at most `len`, and the buffer has
130+
// capacity for `len + BLOCK`.
131+
unsafe { out.add(at).cast::<[u64; BLOCK]>().write_unaligned(block) };
132+
at += BLOCK;
133+
if at >= end {
134+
break;
135+
}
136+
block = block.map(|index| index + BLOCK as u64);
137+
}
138+
start = end;
139+
}
140+
// SAFETY: every position below `start` was written by the list that holds it.
141+
unsafe { indices.set_len(start) };
142+
Ok(indices.freeze().into_array())
143+
}
144+
145+
/// Each capture's one value per list, repeated for each of that list's elements: sizes
146+
/// `[2, 0, 3]` turn `[a, b, c]` into `[a, a, c, c, c]`.
147+
pub(super) fn repeat(
148+
&self,
149+
captures: Vec<ArrayRef>,
150+
ctx: &mut ExecutionCtx,
151+
) -> VortexResult<Vec<ArrayRef>> {
152+
// Which list each element is in, built once for all the captures that are not constant.
153+
let lists = if captures.iter().all(|values| values.as_constant().is_some()) {
154+
None
155+
} else {
156+
Some(self.lists(ctx)?)
157+
};
158+
let mut repeated = Vec::with_capacity(captures.len());
159+
for values in captures {
160+
repeated.push(match (values.as_constant(), &lists) {
161+
(Some(value), _) => ConstantArray::new(value, self.len()).into_array(),
162+
// TODO(mk): repeat each value as one run of a run-end array instead of copying it
163+
// once per element, once run-end lives in vortex-array.
164+
// The body may read the repeated values once for every slice it takes, so compute
165+
// them once.
166+
(None, Some(lists)) => values
167+
.take(lists.clone())?
168+
.execute::<Canonical>(ctx)?
169+
.into_array(),
170+
(None, None) => {
171+
unreachable!("the lists are numbered when a capture is not constant")
172+
}
173+
});
174+
}
175+
Ok(repeated)
176+
}
177+
178+
/// Which list each element is in: sizes `[2, 0, 3]` give `[0, 0, 2, 2, 2]`, in the narrowest
179+
/// unsigned type that numbers every list.
180+
fn lists(&self, ctx: &mut ExecutionCtx) -> VortexResult<ArrayRef> {
181+
let sizes = self.sizes(ctx)?;
182+
let last = sizes.len().saturating_sub(1);
183+
if u8::try_from(last).is_ok() {
184+
list_numbers::<u8>(&sizes, self.len())
185+
} else if u16::try_from(last).is_ok() {
186+
list_numbers::<u16>(&sizes, self.len())
187+
} else if u32::try_from(last).is_ok() {
188+
list_numbers::<u32>(&sizes, self.len())
189+
} else {
190+
list_numbers::<u64>(&sizes, self.len())
191+
}
192+
}
193+
194+
/// The lists again, with one value per element as their new elements.
195+
pub(super) fn rebuild(self, values: ArrayRef) -> VortexResult<ArrayRef> {
196+
vortex_ensure!(
197+
values.len() == self.len(),
198+
"expected one value per element: {} values for {} elements",
199+
values.len(),
200+
self.len()
201+
);
202+
// SAFETY: the offsets start at 0, never decrease, and end at the number of elements,
203+
// which is `values.len()`; the validity is the input lists'.
204+
Ok(unsafe { ListArray::new_unchecked(values, self.offsets, self.validity) }.into_array())
205+
}
206+
}
207+
208+
/// Whether a null list holds elements, which a lambda that can fail must not see.
209+
fn null_lists_hold_elements(
210+
offsets: &ArrayRef,
211+
validity: &Validity,
212+
ctx: &mut ExecutionCtx,
213+
) -> VortexResult<bool> {
214+
if matches!(validity, Validity::NonNullable | Validity::AllValid) {
215+
return Ok(false);
216+
}
217+
let offsets = offsets.clone().execute::<PrimitiveArray>(ctx)?;
218+
let valid = validity.execute_mask(offsets.len() - 1, ctx)?;
219+
Ok(match_each_integer_ptype!(offsets.ptype(), |T| {
220+
offsets
221+
.as_slice::<T>()
222+
.windows(2)
223+
.enumerate()
224+
.any(|(list, bound)| bound[0] != bound[1] && !valid.value(list))
225+
}))
226+
}
227+
228+
/// Each list's number, once for each of its elements, for lists of the given sizes.
229+
fn list_numbers<T: NativePType>(sizes: &[u64], len: usize) -> VortexResult<ArrayRef>
230+
where
231+
usize: AsPrimitive<T>,
232+
{
233+
let mut numbers = BufferMut::<T>::with_capacity(len);
234+
for (list, &size) in sizes.iter().enumerate() {
235+
numbers.push_n(list.as_(), usize::try_from(size)?);
236+
}
237+
Ok(numbers.freeze().into_array())
238+
}
239+
240+
/// The elements, the start and size of each list's range of them, and the lists' validity.
241+
type ListParts = (ArrayRef, Vec<u64>, Vec<u64>, Validity);
242+
243+
fn list_parts(list: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<ListParts> {
244+
if let Some(list) = list.as_opt::<List>() {
245+
let bounds = to_u64s(list.offsets(), ctx)?;
246+
let starts = bounds[..list.len()].to_vec();
247+
let sizes = bounds.windows(2).map(|bound| bound[1] - bound[0]).collect();
248+
return Ok((list.elements().clone(), starts, sizes, list.list_validity()));
249+
}
250+
if let DType::FixedSizeList(_, size, _) = list.dtype() {
251+
let size = u64::from(*size);
252+
let list = list.execute::<FixedSizeListArray>(ctx)?;
253+
let starts = (0..list.len() as u64).map(|i| i * size).collect();
254+
let sizes = vec![size; list.len()];
255+
let validity = list.fixed_size_list_validity();
256+
return Ok((list.elements().clone(), starts, sizes, validity));
257+
}
258+
let list = list.execute::<ListViewArray>(ctx)?;
259+
let starts = to_u64s(list.offsets(), ctx)?;
260+
let sizes = to_u64s(list.sizes(), ctx)?;
261+
let validity = list.listview_validity();
262+
Ok((list.elements().clone(), starts, sizes, validity))
263+
}
264+
265+
/// The elements at `starts[i]..starts[i] + sizes[i]` of each list, in list order.
266+
fn gather(elements: ArrayRef, starts: &[u64], sizes: &[u64]) -> VortexResult<ArrayRef> {
267+
let mut range: Option<(u64, u64)> = None;
268+
let (mut back_to_back, mut in_order) = (true, true);
269+
for (&start, &size) in starts.iter().zip(sizes).filter(|(_, size)| **size > 0) {
270+
if let Some((_, end)) = range {
271+
back_to_back &= start == end;
272+
in_order &= start >= end;
273+
}
274+
range = Some((range.map_or(start, |(first, _)| first), start + size));
275+
}
276+
277+
let Some((first, end)) = range else {
278+
return elements.slice(0..0);
279+
};
280+
if back_to_back {
281+
return elements.slice(usize::try_from(first)?..usize::try_from(end)?);
282+
}
283+
if in_order {
284+
// Copy the ranges, keeping their order: a filter by a mask of them.
285+
let mut included = BitBufferMut::new_unset(elements.len());
286+
for (&start, &size) in starts.iter().zip(sizes) {
287+
included.fill_range(
288+
usize::try_from(start)?,
289+
usize::try_from(start + size)?,
290+
true,
291+
);
292+
}
293+
return elements.filter(Mask::from(included.freeze()));
294+
}
295+
// Ranges that overlap or run out of order, as in a list view.
296+
let total = usize::try_from(sizes.iter().sum::<u64>())?;
297+
let mut indices = BufferMut::<u64>::with_capacity(total);
298+
for (&start, &size) in starts.iter().zip(sizes) {
299+
indices.extend(start..start + size);
300+
}
301+
elements.take(indices.freeze().into_array())
302+
}
303+
304+
fn to_u64s(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Vec<u64>> {
305+
let array = array.clone().execute::<PrimitiveArray>(ctx)?;
306+
Ok(match_each_integer_ptype!(array.ptype(), |T| {
307+
array
308+
.as_slice::<T>()
309+
.iter()
310+
.map(|&value| value.as_())
311+
.collect()
312+
}))
313+
}

0 commit comments

Comments
 (0)