Skip to content

Commit 6f04986

Browse files
committed
capture values read from outside list_transform lambdas
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent c546c66 commit 6f04986

8 files changed

Lines changed: 569 additions & 113 deletions

File tree

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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;
@@ -1397,16 +1398,27 @@ pub fn bound_list_sum_opts(
13971398

13981399
/// Creates an expression that applies a lambda to every element of each list.
13991400
///
1400-
/// The lambda takes one parameter, the element.
1401+
/// The lambda takes one parameter, the element. Whatever else its body reads from outside, such as
1402+
/// a column or a parameter of an enclosing lambda, becomes another input of the call and another
1403+
/// parameter of the lambda (see [`Lambda::extract_captures`]): every element of a row's list sees
1404+
/// that row's value.
14011405
///
14021406
/// ```rust
14031407
/// # use vortex_array::expr::{Lambda, checked_add, col, list_transform, lit, var};
14041408
/// let lambda = Lambda::new(["x"], checked_add(var("x"), lit(1_i32))).unwrap();
14051409
/// let expr = list_transform(col("l"), lambda);
14061410
/// assert_eq!(expr.to_string(), "vortex.list.transform($.l, (x) -> (@x + 1i32))");
1411+
///
1412+
/// let lambda = Lambda::new(["x"], checked_add(var("x"), col("a"))).unwrap();
1413+
/// let expr = list_transform(col("l"), lambda);
1414+
/// assert_eq!(expr.to_string(), "vortex.list.transform($.l, $.a, (x, a) -> (@x + @a))");
14071415
/// ```
14081416
pub fn list_transform(list: Expression, lambda: Lambda) -> Expression {
1409-
ListTransform.new_expr(ListTransformOptions::Lambda(lambda), [list])
1417+
let (lambda, captures) = lambda.extract_captures();
1418+
ListTransform.new_expr(
1419+
ListTransformOptions::Lambda(lambda),
1420+
iter::once(list).chain(captures),
1421+
)
14101422
}
14111423

14121424
/// Reinterpret timestamp wall times in `options.time_zone`, preserving the time unit.

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

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
//!
1111
//! Binding turns the lambda into an ordinary expression over a struct of its inputs, one field per
1212
//! parameter: `@x` becomes `$.x`. To run it, the function puts the parameters' arrays in that
13-
//! struct, such as one row per list element, and applies the bound body to it. The body reads
14-
//! nothing but its parameters, so the function's children are everything it reads.
13+
//! struct, such as one row per list element, and applies the bound body to it.
14+
//!
15+
//! A body may read values from outside the lambda, such as a column. Before a function keeps a
16+
//! lambda, [`Lambda::extract_captures`] turns each of those values into a new parameter and returns
17+
//! the expressions that compute them, which the function takes as extra children. A lambda in a
18+
//! function's options therefore reads nothing but its parameters, and the function's children are
19+
//! everything it reads.
1520
1621
#[cfg(test)]
1722
mod tests;
@@ -25,6 +30,7 @@ use vortex_error::VortexExpect;
2530
use vortex_error::VortexResult;
2631
use vortex_error::vortex_bail;
2732
use vortex_error::vortex_ensure;
33+
use vortex_error::vortex_ensure_eq;
2834

2935
use crate::ArrayRef;
3036
use crate::IntoArray;
@@ -42,14 +48,17 @@ use crate::expr::traversal::NodeExt;
4248
use crate::expr::traversal::Transformed;
4349
use crate::expr::traversal::TraversalOrder;
4450
use crate::expr::traversal::pre_order_visit_down;
51+
use crate::expr::var;
52+
use crate::scalar_fn::fns::get_item::GetItem;
4553
use crate::scalar_fn::internal::row_count::RowCount;
4654
use crate::validity::Validity;
4755

4856
/// Parameter names and a body: the function argument of a higher-order scalar function, as
4957
/// written.
5058
///
5159
/// The body is an ordinary [`Expression`] that reads its parameters with
52-
/// [`var`](crate::expr::var), and nothing else from outside the lambda.
60+
/// [`var`](crate::expr::var). It may also read values from outside the lambda, which
61+
/// [`Lambda::extract_captures`] turns into parameters.
5362
///
5463
/// ```rust
5564
/// # use vortex_array::expr::{Lambda, checked_add, lit, var};
@@ -65,8 +74,10 @@ pub struct Lambda {
6574
impl Lambda {
6675
/// Create a lambda with these parameters and body.
6776
///
68-
/// Returns an error when a parameter repeats, or when the body reads from outside the lambda:
69-
/// `$` or a column, another lambda's parameter, or the scan's `row_idx()` or `row_count()`.
77+
/// The body may read from outside the lambda: `$` or a column, another lambda's parameter, or
78+
/// the scan's `row_idx()` or `row_count()`. See [`Lambda::extract_captures`].
79+
///
80+
/// Returns an error when a parameter repeats.
7081
pub fn new(
7182
params: impl IntoIterator<Item = impl Into<FieldName>>,
7283
body: Expression,
@@ -75,19 +86,61 @@ impl Lambda {
7586
if let Some(param) = params.iter().duplicates().next() {
7687
vortex_bail!("lambda parameter {param} is declared more than once");
7788
}
78-
pre_order_visit_down(&body, |node: &Expression| {
79-
vortex_ensure!(
80-
!reads_outside_lambda(node, params.as_ref()),
81-
"lambda ({}) can only read its parameters, not {node}; reading anything else is \
82-
not supported yet",
83-
params.iter().format(", ")
84-
);
85-
Ok(TraversalOrder::Continue)
86-
})?;
87-
8889
Ok(Self { params, body })
8990
}
9091

92+
/// Turn every value the body reads from outside the lambda into a new parameter.
93+
///
94+
/// Returns the lambda with those parameters after its own, and the expressions that compute
95+
/// their values, in the same order. A function that takes a lambda keeps the returned lambda
96+
/// and takes the expressions as extra children, so that a rewrite of the expression, such as
97+
/// `replace` or field pruning, sees them.
98+
///
99+
/// A value is read as a whole: `$.s.a` becomes one parameter, `a`, not a read of `$`. A value
100+
/// read twice becomes one parameter. A new parameter is named after the field or variable it
101+
/// reads, with a number added when that name is taken.
102+
///
103+
/// ```rust
104+
/// # use vortex_array::expr::{Lambda, checked_add, col, var};
105+
/// let lambda = Lambda::new(["x"], checked_add(var("x"), col("a"))).unwrap();
106+
/// let (lambda, captures) = lambda.extract_captures();
107+
/// assert_eq!(lambda.to_string(), "(x, a) -> (@x + @a)");
108+
/// assert_eq!(captures, [col("a")]);
109+
/// ```
110+
pub fn extract_captures(self) -> (Lambda, Vec<Expression>) {
111+
let written = self.params;
112+
let mut params = written.iter().cloned().collect_vec();
113+
let mut captures: Vec<Expression> = Vec::new();
114+
let body = self
115+
.body
116+
.transform_down(|node| {
117+
if !is_capture(&node, written.as_ref()) {
118+
return Ok(Transformed::no(node));
119+
}
120+
let name = match captures.iter().position(|capture| capture == &node) {
121+
Some(index) => params[written.len() + index].clone(),
122+
None => {
123+
let name = unused_name(capture_name(&node), &params);
124+
params.push(name.clone());
125+
captures.push(node);
126+
name
127+
}
128+
};
129+
Ok(Transformed {
130+
value: var(name),
131+
changed: true,
132+
order: TraversalOrder::Skip,
133+
})
134+
})
135+
.vortex_expect("expression traversal cannot fail")
136+
.into_inner();
137+
let lambda = Lambda {
138+
params: params.into(),
139+
body,
140+
};
141+
(lambda, captures)
142+
}
143+
91144
/// The parameter names, in order.
92145
pub fn params(&self) -> &[FieldName] {
93146
self.params.as_ref()
@@ -101,18 +154,21 @@ impl Lambda {
101154
/// The dtype of the body, given the parameters' dtypes, without binding it.
102155
pub fn return_dtype(&self, param_dtypes: &[DType]) -> VortexResult<DType> {
103156
let inputs = inputs_dtype(self.param_fields(param_dtypes)?);
104-
self.body_over_inputs().return_dtype(&inputs)
157+
self.body_over_inputs()?.return_dtype(&inputs)
105158
}
106159

107160
/// Bind the body to these parameter dtypes: `@x` becomes `$.x`, a field of the struct of the
108161
/// lambda's inputs.
109162
///
110163
/// A function that takes a lambda calls this from its `bind_options`, so the body is bound
111164
/// once, with the node that holds it, and never while the function runs.
165+
///
166+
/// Returns an error when the body reads from outside the lambda: the function must pass those
167+
/// values in as parameters, with [`Lambda::extract_captures`].
112168
pub fn bind(&self, param_dtypes: &[DType]) -> VortexResult<BoundLambda> {
113169
let params = self.param_fields(param_dtypes)?;
114170
let body = self
115-
.body_over_inputs()
171+
.body_over_inputs()?
116172
.bind(&inputs_dtype(params.clone()))?;
117173
Ok(BoundLambda { params, body })
118174
}
@@ -149,12 +205,20 @@ impl Lambda {
149205

150206
/// The body over the struct of the lambda's inputs, where `@x` reads the field `x`.
151207
///
208+
/// Returns an error when the body reads from outside the lambda, which the struct can't hold.
152209
/// A lambda in the body is a function's options, which this leaves alone: its variables are its
153210
/// own parameters.
154-
fn body_over_inputs(&self) -> Expression {
155-
self.body
211+
fn body_over_inputs(&self) -> VortexResult<Expression> {
212+
Ok(self
213+
.body
156214
.clone()
157215
.transform_down(|node| {
216+
vortex_ensure!(
217+
!reads_outside_lambda(&node, self.params.as_ref()),
218+
"lambda ({}) can only read its parameters, not {node}; \
219+
Lambda::extract_captures turns what it reads from outside into parameters",
220+
self.params.iter().format(", ")
221+
);
158222
Ok(match &node {
159223
Expression::Variable(name) => Transformed {
160224
value: get_item(name.clone(), root()),
@@ -163,9 +227,8 @@ impl Lambda {
163227
},
164228
_ => Transformed::no(node),
165229
})
166-
})
167-
.vortex_expect("expression traversal cannot fail")
168-
.into_inner()
230+
})?
231+
.into_inner())
169232
}
170233
}
171234

@@ -211,6 +274,19 @@ impl BoundLambda {
211274
self.params.fields().eq(param_dtypes.iter().cloned())
212275
}
213276

277+
/// The same lambda bound to other parameter dtypes, such as after a rewrite of the function's
278+
/// node made a value it passes in non-nullable.
279+
///
280+
/// Every node of the body is rebuilt over the new struct of inputs, so the body's dtype follows
281+
/// the parameters'. The result is the same as binding the written lambda to these dtypes.
282+
pub fn rebind(&self, param_dtypes: &[DType]) -> VortexResult<BoundLambda> {
283+
vortex_ensure_eq!(param_dtypes.len(), self.params.nfields(), "lambda {self}");
284+
let params = StructFields::new(self.params.names().clone(), param_dtypes.to_vec());
285+
let root = BoundExpression::new_root(inputs_dtype(params.clone()));
286+
let body = with_root(&self.body, &root)?;
287+
Ok(BoundLambda { params, body })
288+
}
289+
214290
/// Whether nothing in the body can fail. See [`Lambda::is_infallible`].
215291
pub fn is_infallible(&self) -> bool {
216292
let mut infallible = true;
@@ -274,10 +350,23 @@ impl Display for BoundLambda {
274350
}
275351
}
276352

353+
/// `expr` rebuilt over another root, node by node, so that every node's dtype follows the root's.
354+
fn with_root(expr: &BoundExpression, root: &BoundExpression) -> VortexResult<BoundExpression> {
355+
if expr.as_scalar().is_none() {
356+
return Ok(root.clone());
357+
}
358+
let children: Vec<_> = expr
359+
.children()
360+
.iter()
361+
.map(|child| with_root(child, root))
362+
.try_collect()?;
363+
expr.clone().with_children(children)
364+
}
365+
277366
/// Whether `node`, in the body of a lambda with these parameters, reads a value from outside the
278-
/// lambda, which lambdas can't do yet: `$`, another lambda's parameter, or the scan's `row_idx()`
279-
/// or `row_count()`. The scan replaces those two before anything runs, but never looks inside a
280-
/// lambda's body.
367+
/// lambda: `$`, another lambda's parameter, or the scan's `row_idx()` or `row_count()`. The scan
368+
/// replaces those two before anything runs, but never looks inside a lambda's body, so they too
369+
/// must be passed in as parameters.
281370
fn reads_outside_lambda(node: &Expression, params: &[FieldName]) -> bool {
282371
match node {
283372
Expression::Root => true,
@@ -288,3 +377,47 @@ fn reads_outside_lambda(node: &Expression, params: &[FieldName]) -> bool {
288377
}
289378
}
290379
}
380+
381+
/// Whether `node` is a value from outside a lambda with these parameters, read as a whole: a read
382+
/// from outside, or a field of one, such as `$.s.a`.
383+
fn is_capture(node: &Expression, params: &[FieldName]) -> bool {
384+
if node
385+
.as_scalar()
386+
.is_some_and(|scalar_fn| scalar_fn.is::<GetItem>())
387+
{
388+
return is_capture(&node.children()[0], params);
389+
}
390+
reads_outside_lambda(node, params)
391+
}
392+
393+
/// The name of the parameter for a captured value: the field or variable it reads, such as `a`
394+
/// for `$.s.a`.
395+
fn capture_name(capture: &Expression) -> FieldName {
396+
match capture {
397+
Expression::Root => FieldName::from("root"),
398+
Expression::Variable(name) => name.clone(),
399+
Expression::Scalar { scalar_fn, .. } => match scalar_fn.as_opt::<GetItem>() {
400+
Some(field) => field.clone(),
401+
// `row_idx()` or `row_count()`, named after their ids.
402+
None => {
403+
let id = scalar_fn.id();
404+
let (_, name) = id
405+
.as_ref()
406+
.rsplit_once('.')
407+
.unwrap_or_else(|| ("", id.as_ref()));
408+
FieldName::from(name)
409+
}
410+
},
411+
}
412+
}
413+
414+
/// `name`, or `name_2`, `name_3`, ... if it is taken.
415+
fn unused_name(name: FieldName, taken: &[FieldName]) -> FieldName {
416+
if !taken.contains(&name) {
417+
return name;
418+
}
419+
(2..)
420+
.map(|n| FieldName::from(format!("{name}_{n}")))
421+
.find(|candidate| !taken.contains(candidate))
422+
.vortex_expect("some numbered name is free")
423+
}

‎vortex-array/src/expr/lambda/tests.rs‎

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,57 @@ fn new_keeps_params_and_body() -> VortexResult<()> {
168168
Ok(())
169169
}
170170

171+
#[rstest]
172+
#[case::column(checked_add(var("x"), col("a")), "(x, a) -> (@x + @a)", vec![col("a")])]
173+
#[case::field_of_a_column(
174+
checked_add(var("x"), get_item("i", col("s"))),
175+
"(x, i) -> (@x + @i)",
176+
vec![get_item("i", col("s"))]
177+
)]
178+
#[case::read_twice(checked_add(col("a"), col("a")), "(x, a) -> (@a + @a)", vec![col("a")])]
179+
#[case::name_taken(checked_add(var("x"), col("x")), "(x, x_2) -> (@x + @x_2)", vec![col("x")])]
180+
#[case::root(eq(var("x"), root()), "(x, root) -> (@x = @root)", vec![root()])]
181+
#[case::other_variable(
182+
checked_add(var("x"), get_item("k", var("p"))),
183+
"(x, k) -> (@x + @k)",
184+
vec![get_item("k", var("p"))]
185+
)]
186+
#[case::function_of_the_scan(
187+
checked_add(var("x"), RowCount.new_expr(EmptyOptions, [])),
188+
"(x, row_count) -> (@x + @row_count)",
189+
vec![RowCount.new_expr(EmptyOptions, [])]
190+
)]
191+
#[case::field_of_a_parameter(get_item("k", var("x")), "(x) -> @x.k", vec![])]
192+
fn extract_captures_turns_reads_from_outside_into_parameters(
193+
#[case] body: Expression,
194+
#[case] lambda: &str,
195+
#[case] captures: Vec<Expression>,
196+
) -> VortexResult<()> {
197+
let (extracted, extracted_captures) = Lambda::new(["x"], body)?.extract_captures();
198+
assert_eq!(extracted.to_string(), lambda);
199+
assert_eq!(extracted_captures, captures);
200+
Ok(())
201+
}
202+
171203
#[rstest]
172204
#[case::root(checked_add(var("x"), root()), "not $;")]
173205
#[case::column(checked_add(var("x"), col("a")), "not $;")]
174206
#[case::other_variable(checked_add(var("x"), var("p")), "not @p;")]
175207
#[case::field_of_another_variable(get_item("k", var("p")), "not @p;")]
176208
#[case::function_of_the_scan(RowCount.new_expr(EmptyOptions, []), "not row_count();")]
177-
fn a_body_reads_only_its_parameters(#[case] body: Expression, #[case] read: &str) {
178-
let error = Lambda::new(["x"], body).expect_err("reads from outside");
209+
fn binding_a_body_that_reads_from_outside_is_an_error(
210+
#[case] body: Expression,
211+
#[case] read: &str,
212+
) -> VortexResult<()> {
213+
let error = Lambda::new(["x"], body)?
214+
.bind(&[i32_()])
215+
.expect_err("reads from outside");
179216
let message = error.to_string();
180217
assert!(
181218
message.contains("lambda (x) can only read its parameters") && message.contains(read),
182219
"{message}"
183220
);
221+
Ok(())
184222
}
185223

186224
#[test]
@@ -295,6 +333,15 @@ fn binding_types_the_body_by_its_parameters() -> VortexResult<()> {
295333
Ok(())
296334
}
297335

336+
#[test]
337+
fn rebinding_is_binding_to_the_new_dtypes() -> VortexResult<()> {
338+
let lambda = Lambda::new(["x"], checked_add(var("x"), lit(1_i32)))?;
339+
let rebound = lambda.bind(&[i32_()])?.rebind(&[i32_().as_nullable()])?;
340+
assert_eq!(rebound, lambda.bind(&[i32_().as_nullable()])?);
341+
assert_eq!(rebound.dtype(), &i32_().as_nullable());
342+
Ok(())
343+
}
344+
298345
#[test]
299346
fn binding_an_expression_binds_its_lambdas() -> VortexResult<()> {
300347
let expr = apply(col("a"), "x", checked_add(var("x"), lit(1_i32)))?;

0 commit comments

Comments
 (0)