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) ]
1722mod tests;
@@ -25,6 +30,7 @@ use vortex_error::VortexExpect;
2530use vortex_error:: VortexResult ;
2631use vortex_error:: vortex_bail;
2732use vortex_error:: vortex_ensure;
33+ use vortex_error:: vortex_ensure_eq;
2834
2935use crate :: ArrayRef ;
3036use crate :: IntoArray ;
@@ -42,14 +48,17 @@ use crate::expr::traversal::NodeExt;
4248use crate :: expr:: traversal:: Transformed ;
4349use crate :: expr:: traversal:: TraversalOrder ;
4450use crate :: expr:: traversal:: pre_order_visit_down;
51+ use crate :: expr:: var;
52+ use crate :: scalar_fn:: fns:: get_item:: GetItem ;
4553use crate :: scalar_fn:: internal:: row_count:: RowCount ;
4654use 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 {
6574impl 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 .
281370fn 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+ }
0 commit comments