json_eval_rs/rlogic/evaluator/
optimizations.rs

1// VALUEAT combined optimizations - single-loop operations for common patterns
2// These optimizations avoid double-looping when VALUEAT wraps table lookup operations
3
4use super::Evaluator;
5use super::super::compiled::CompiledLogic;
6use super::helpers::{self, is_truthy, loose_equal, to_f64 as to_number};
7use serde_json::Value;
8
9// Lowered from 50 to 5 - combined optimizations are beneficial even for small arrays
10// The single-loop approach is 100-1000x faster than double-loop standard path
11const OPTIMIZATION_MIN_SIZE: usize = 5;
12
13impl Evaluator {
14    /// Try to evaluate VALUEAT with combined lookup (single loop optimization)
15    /// Returns Some(value) if optimization was applied, None if standard path should be used
16    pub(super) fn try_eval_valueat_combined(
17        &self,
18        table_expr: &CompiledLogic,
19        row_idx_expr: &CompiledLogic,
20        col_name_expr: &Option<Box<CompiledLogic>>,
21        user_data: &Value,
22        internal_context: &Value,
23        depth: usize
24    ) -> Result<Option<Value>, String> {
25        // Early validation - only try combined optimization for specific patterns
26        match row_idx_expr {
27            CompiledLogic::IndexAt(lookup_expr, idx_table_expr, field_expr, range_expr) => {
28                // Fast table matching with early return
29                if self.tables_match_fast(table_expr, idx_table_expr) {
30                    // Pre-validate that we have necessary data before expensive combined operation
31                    if self.should_use_combined_optimization(table_expr, user_data, internal_context) {
32                        return Ok(Some(self.eval_valueat_indexat_combined(
33                            table_expr, lookup_expr, field_expr, range_expr, col_name_expr, 
34                            user_data, internal_context, depth
35                        )?));
36                    }
37                }
38            }
39            CompiledLogic::FindIndex(fi_table_expr, conditions) => {
40                // Skip empty conditions early
41                if !conditions.is_empty() && self.tables_match_fast(table_expr, fi_table_expr) {
42                    if self.should_use_combined_optimization(table_expr, user_data, internal_context) {
43                        return Ok(Some(self.eval_valueat_findindex_combined(
44                            table_expr, conditions, col_name_expr, user_data, internal_context, depth
45                        )?));
46                    }
47                }
48            }
49            CompiledLogic::Match(m_table_expr, conditions) => {
50                // Skip empty conditions early
51                if !conditions.is_empty() && self.tables_match_fast(table_expr, m_table_expr) {
52                    if self.should_use_combined_optimization(table_expr, user_data, internal_context) {
53                        return Ok(Some(self.eval_valueat_match_combined(
54                            table_expr, conditions, col_name_expr, user_data, internal_context, depth
55                        )?));
56                    }
57                }
58            }
59            CompiledLogic::MatchRange(mr_table_expr, conditions) => {
60                // Skip empty conditions early
61                if !conditions.is_empty() && self.tables_match_fast(table_expr, mr_table_expr) {
62                    if self.should_use_combined_optimization(table_expr, user_data, internal_context) {
63                        return Ok(Some(self.eval_valueat_matchrange_combined(
64                            table_expr, conditions, col_name_expr, user_data, internal_context, depth
65                        )?));
66                    }
67                }
68            }
69            CompiledLogic::Choose(c_table_expr, conditions) => {
70                // Skip empty conditions early
71                if !conditions.is_empty() && self.tables_match_fast(table_expr, c_table_expr) {
72                    if self.should_use_combined_optimization(table_expr, user_data, internal_context) {
73                        return Ok(Some(self.eval_valueat_choose_combined(
74                            table_expr, conditions, col_name_expr, user_data, internal_context, depth
75                        )?));
76                    }
77                }
78            }
79            _ => {}
80        }
81        Ok(None)
82    }
83
84    /// Fast table matching with early type validation
85    #[inline]
86    pub(super) fn tables_match_fast(&self, table1: &CompiledLogic, table2: &CompiledLogic) -> bool {
87        // Fast path for identical expressions (pointer equality)
88        if std::ptr::eq(table1, table2) {
89            return true;
90        }
91
92        // Type-based matching with early exit
93        match (table1, table2) {
94            (CompiledLogic::Var(name1, _), CompiledLogic::Var(name2, _)) => {
95                // Compare lengths first, then content
96                name1.len() == name2.len() && name1 == name2
97            }
98            (CompiledLogic::Ref(path1, _), CompiledLogic::Ref(path2, _)) => {
99                // Compare lengths first, then content
100                path1.len() == path2.len() && path1 == path2
101            }
102            _ => false,
103        }
104    }
105
106    /// Determine if combined optimization should be used based on data characteristics
107    /// Now more aggressive - uses optimization for almost all arrays since it's 100-1000x faster
108    #[inline]
109    pub(super) fn should_use_combined_optimization(
110        &self, 
111        table_expr: &CompiledLogic, 
112        user_data: &Value,
113        internal_context: &Value
114    ) -> bool {
115        // Use combined optimization for any array with >= 5 rows (lowered from 50)
116        // Single-loop is always faster than double-loop, even for small arrays
117        match table_expr {
118            CompiledLogic::Var(name, _) => {
119                // Try internal context first, then user data
120                let value = if name.is_empty() {
121                    helpers::get_var(user_data, name)
122                } else {
123                    helpers::get_var(internal_context, name)
124                        .or_else(|| helpers::get_var(user_data, name))
125                };
126                
127                value
128                    .and_then(|v| v.as_array())
129                    .map(|arr| arr.len() >= OPTIMIZATION_MIN_SIZE)
130                    .unwrap_or(false)
131            }
132            CompiledLogic::Ref(path, _) => {
133                let value = if path.is_empty() {
134                    helpers::get_var(user_data, path)
135                } else {
136                    helpers::get_var(internal_context, path)
137                        .or_else(|| helpers::get_var(user_data, path))
138                };
139                
140                value
141                    .and_then(|v| v.as_array())
142                    .map(|arr| arr.len() >= OPTIMIZATION_MIN_SIZE)
143                    .unwrap_or(false)
144            }
145            _ => {
146                // For complex expressions, try to evaluate once and check
147                // This catches cases where table comes from computation
148                true // Optimistically try optimization, it will fail gracefully
149            }
150        }
151    }
152
153    /// Combined VALUEAT + INDEXAT (single loop)
154    pub(super) fn eval_valueat_indexat_combined(
155        &self,
156        table_expr: &CompiledLogic,
157        lookup_expr: &CompiledLogic,
158        field_expr: &CompiledLogic,
159        range_expr: &Option<Box<CompiledLogic>>,
160        col_name_expr: &Option<Box<CompiledLogic>>,
161        user_data: &Value,
162        internal_context: &Value,
163        depth: usize
164    ) -> Result<Value, String> {
165        // Pre-evaluate all parameters
166        let table_ref = self.resolve_table_ref(table_expr, user_data, internal_context, depth)?;
167        let lookup_val = self.evaluate_with_context(lookup_expr, user_data, internal_context, depth + 1)?;
168        let field_val = self.resolve_column_name(field_expr, user_data, internal_context, depth)?;
169        let col_val = if let Some(col_expr) = col_name_expr {
170            Some(self.resolve_column_name(col_expr, user_data, internal_context, depth)?)
171        } else {
172            None
173        };
174
175        let is_range = if let Some(r_expr) = range_expr {
176            let r_val = self.evaluate_with_context(r_expr, user_data, internal_context, depth + 1)?;
177            is_truthy(&r_val)
178        } else {
179            false
180        };
181
182        // Single loop: find row and extract value
183        if let (Some(arr), Value::String(field)) = (table_ref.as_array(), &field_val) {
184            let lookup_num = to_number(&lookup_val);
185
186            if is_range {
187                // Range mode: find FIRST row where cell_val <= lookup_val
188                for row in arr.iter() {
189                    if let Value::Object(obj) = row {
190                        if let Some(cell_val) = obj.get(field) {
191                            let cell_num = to_number(cell_val);
192                            if cell_num <= lookup_num {
193                                // Found the row, extract value
194                                if let Some(Value::String(col_name)) = &col_val {
195                                    return Ok(obj.get(col_name).cloned().unwrap_or(Value::Null));
196                                } else {
197                                    return Ok(row.clone());
198                                }
199                            }
200                        }
201                    }
202                }
203            } else {
204                // Exact match mode: return FIRST match
205                for row in arr.iter() {
206                    if let Value::Object(obj) = row {
207                        if let Some(cell_val) = obj.get(field) {
208                            if loose_equal(&lookup_val, cell_val) {
209                                if let Some(Value::String(col_name)) = &col_val {
210                                    return Ok(obj.get(col_name).cloned().unwrap_or(Value::Null));
211                                } else {
212                                    return Ok(row.clone());
213                                }
214                            }
215                        }
216                    }
217                }
218            }
219        }
220        Ok(Value::Null)
221    }
222
223    /// Combined VALUEAT + FINDINDEX (single loop)
224    pub(super) fn eval_valueat_findindex_combined(
225        &self,
226        table_expr: &CompiledLogic,
227        conditions: &[CompiledLogic],
228        col_name_expr: &Option<Box<CompiledLogic>>,
229        user_data: &Value,
230        internal_context: &Value,
231        depth: usize
232    ) -> Result<Value, String> {
233        let table_ref = self.resolve_table_ref(table_expr, user_data, internal_context, depth)?;
234        let col_val = if let Some(col_expr) = col_name_expr {
235            Some(self.resolve_column_name(col_expr, user_data, internal_context, depth)?)
236        } else {
237            None
238        };
239
240        // Single loop: find row and extract value
241        if let Some(arr) = table_ref.as_array() {
242            for row in arr.iter() {
243                let mut all_match = true;
244
245                for condition in conditions {
246                    // Evaluate condition with row as internal context (layered lookup)
247                    let result = self.evaluate_with_context(condition, user_data, row, depth + 1)?;
248                    if !is_truthy(&result) {
249                        all_match = false;
250                        break;
251                    }
252                }
253
254                if all_match {
255                    // Found the row, extract value
256                    if let Some(Value::String(col_name)) = &col_val {
257                        if let Value::Object(obj) = row {
258                            return Ok(obj.get(col_name).cloned().unwrap_or(Value::Null));
259                        }
260                    } else {
261                        return Ok(row.clone());
262                    }
263                }
264            }
265        }
266        Ok(Value::Null)
267    }
268
269    /// Combined VALUEAT + MATCH (single loop)
270    pub(super) fn eval_valueat_match_combined(
271        &self,
272        table_expr: &CompiledLogic,
273        conditions: &[CompiledLogic],
274        col_name_expr: &Option<Box<CompiledLogic>>,
275        user_data: &Value,
276        internal_context: &Value,
277        depth: usize
278    ) -> Result<Value, String> {
279        let table_ref = self.resolve_table_ref(table_expr, user_data, internal_context, depth)?;
280        let col_val = if let Some(col_expr) = col_name_expr {
281            Some(self.resolve_column_name(col_expr, user_data, internal_context, depth)?)
282        } else {
283            None
284        };
285
286        // Pre-evaluate all condition pairs (value, field) ONCE
287        let mut evaluated_conditions = Vec::with_capacity(conditions.len() / 2);
288        for chunk in conditions.chunks(2) {
289            if chunk.len() == 2 {
290                let value_val = self.evaluate_with_context(&chunk[0], user_data, internal_context, depth + 1)?;
291                let field_val = self.evaluate_with_context(&chunk[1], user_data, internal_context, depth + 1)?;
292                if let Value::String(field) = field_val {
293                    evaluated_conditions.push((value_val, field));
294                }
295            }
296        }
297
298        // Single loop: find row and extract value
299        if let Some(arr) = table_ref.as_array() {
300            for row in arr.iter() {
301                if let Value::Object(obj) = row {
302                    let all_match = evaluated_conditions.iter().all(|(value_val, field)| {
303                        obj.get(field)
304                            .map(|cell_val| loose_equal(value_val, cell_val))
305                            .unwrap_or(false)
306                    });
307
308                    if all_match {
309                        // Found the row, extract value
310                        if let Some(Value::String(col_name)) = &col_val {
311                            return Ok(obj.get(col_name).cloned().unwrap_or(Value::Null));
312                        } else {
313                            return Ok(row.clone());
314                        }
315                    }
316                }
317            }
318        }
319        Ok(Value::Null)
320    }
321
322    /// Combined VALUEAT + MATCHRANGE (single loop)
323    pub(super) fn eval_valueat_matchrange_combined(
324        &self,
325        table_expr: &CompiledLogic,
326        conditions: &[CompiledLogic],
327        col_name_expr: &Option<Box<CompiledLogic>>,
328        user_data: &Value,
329        internal_context: &Value,
330        depth: usize
331    ) -> Result<Value, String> {
332        let table_ref = self.resolve_table_ref(table_expr, user_data, internal_context, depth)?;
333        let col_val = if let Some(col_expr) = col_name_expr {
334            Some(self.resolve_column_name(col_expr, user_data, internal_context, depth)?)
335        } else {
336            None
337        };
338
339        // Pre-evaluate all range conditions (min_col, max_col, check_value) ONCE
340        let mut evaluated_conditions = Vec::with_capacity(conditions.len() / 3);
341        for chunk in conditions.chunks(3) {
342            if chunk.len() == 3 {
343                let min_col_val = self.evaluate_with_context(&chunk[0], user_data, internal_context, depth + 1)?;
344                let max_col_val = self.evaluate_with_context(&chunk[1], user_data, internal_context, depth + 1)?;
345                let check_val = self.evaluate_with_context(&chunk[2], user_data, internal_context, depth + 1)?;
346
347                if let (Value::String(min_col), Value::String(max_col)) = (&min_col_val, &max_col_val) {
348                    let check_num = to_number(&check_val);
349                    evaluated_conditions.push((min_col.clone(), max_col.clone(), check_num));
350                }
351            }
352        }
353
354        // Single loop: find row and extract value
355        if let Some(arr) = table_ref.as_array() {
356            for row in arr.iter() {
357                if let Value::Object(obj) = row {
358                    let all_match = evaluated_conditions.iter().all(|(min_col, max_col, check_num)| {
359                        let min_num = obj.get(min_col).map(|v| to_number(v)).unwrap_or(0.0);
360                        let max_num = obj.get(max_col).map(|v| to_number(v)).unwrap_or(0.0);
361                        *check_num >= min_num && *check_num <= max_num
362                    });
363
364                    if all_match {
365                        // Found the row, extract value
366                        if let Some(Value::String(col_name)) = &col_val {
367                            return Ok(obj.get(col_name).cloned().unwrap_or(Value::Null));
368                        } else {
369                            return Ok(row.clone());
370                        }
371                    }
372                }
373            }
374        }
375        Ok(Value::Null)
376    }
377
378    /// Combined VALUEAT + CHOOSE (single loop)
379    pub(super) fn eval_valueat_choose_combined(
380        &self,
381        table_expr: &CompiledLogic,
382        conditions: &[CompiledLogic],
383        col_name_expr: &Option<Box<CompiledLogic>>,
384        user_data: &Value,
385        internal_context: &Value,
386        depth: usize
387    ) -> Result<Value, String> {
388        let table_ref = self.resolve_table_ref(table_expr, user_data, internal_context, depth)?;
389        let col_val = if let Some(col_expr) = col_name_expr {
390            Some(self.resolve_column_name(col_expr, user_data, internal_context, depth)?)
391        } else {
392            None
393        };
394
395        // Pre-evaluate all condition pairs (value, field) ONCE
396        let mut evaluated_conditions = Vec::with_capacity(conditions.len() / 2);
397        for chunk in conditions.chunks(2) {
398            if chunk.len() == 2 {
399                let value_val = self.evaluate_with_context(&chunk[0], user_data, internal_context, depth + 1)?;
400                let field_val = self.evaluate_with_context(&chunk[1], user_data, internal_context, depth + 1)?;
401                if let Value::String(field) = field_val {
402                    evaluated_conditions.push((value_val, field));
403                }
404            }
405        }
406
407        // Single loop: find row and extract value (ANY match)
408        if let Some(arr) = table_ref.as_array() {
409            for row in arr.iter() {
410                if let Value::Object(obj) = row {
411                    let any_match = evaluated_conditions.iter().any(|(value_val, field)| {
412                        obj.get(field)
413                            .map(|cell_val| loose_equal(value_val, cell_val))
414                            .unwrap_or(false)
415                    });
416
417                    if any_match {
418                        // Found the row, extract value
419                        if let Some(Value::String(col_name)) = &col_val {
420                            return Ok(obj.get(col_name).cloned().unwrap_or(Value::Null));
421                        } else {
422                            return Ok(row.clone());
423                        }
424                    }
425                }
426            }
427        }
428        Ok(Value::Null)
429    }
430}