json_eval_rs/rlogic/evaluator/
array_lookup.rs

1use super::{Evaluator, types::*};
2use serde_json::Value;
3use super::super::compiled::CompiledLogic;
4use super::helpers;
5
6#[cfg(feature = "parallel")]
7use rayon::prelude::*;
8
9// Lower threshold for parallel processing - even smaller tables benefit from optimization
10#[cfg(feature = "parallel")]
11const PARALLEL_THRESHOLD: usize = 1000;
12
13impl Evaluator {
14    /// Resolve table reference directly - ZERO-COPY with optimized lookup
15    #[inline]
16    pub(super) fn resolve_table_ref<'a>(&self, table_expr: &CompiledLogic, user_data: &'a Value, internal_context: &'a Value, depth: usize) -> Result<TableRef<'a>, String> {
17        match table_expr {
18            CompiledLogic::Var(name, _) => {
19                // OPTIMIZATION: Fast path for common table names (no empty check overhead)
20                let value = if name.is_empty() {
21                    helpers::get_var(user_data, name)
22                } else {
23                    // Try user_data first for tables (internal_context rarely has tables)
24                    helpers::get_var(user_data, name)
25                        .or_else(|| helpers::get_var(internal_context, name))
26                };
27                value
28                    .filter(|v| !v.is_null())
29                    .map(TableRef::Borrowed)
30                    .ok_or_else(|| format!("Variable not found: {}", name))
31            }
32            CompiledLogic::Ref(path, _) => {
33                // Tables are usually in user_data, not internal_context
34                let value = helpers::get_var(user_data, path)
35                    .or_else(|| helpers::get_var(internal_context, path));
36                value
37                    .filter(|v| !v.is_null())
38                    .map(TableRef::Borrowed)
39                    .ok_or_else(|| format!("Reference not found: {}", path))
40            }
41            _ => {
42                self.evaluate_with_context(table_expr, user_data, internal_context, depth + 1)
43                    .map(TableRef::Owned)
44            }
45        }
46    }
47
48    /// Fast path to get borrowed array slice from table expression - ZERO-COPY
49    #[inline]
50    pub(super) fn get_table_array<'a>(&self, table_expr: &CompiledLogic, user_data: &'a Value, internal_context: &'a Value, depth: usize) -> Result<TableRef<'a>, String> {
51        let table_ref = self.resolve_table_ref(table_expr, user_data, internal_context, depth)?;
52        match table_ref.as_value() {
53            Value::Array(_) => Ok(table_ref),
54            Value::Null => Err("Table reference is null".to_string()),
55            _ => Err("Table reference is not an array".to_string()),
56        }
57    }
58
59    /// Resolve column name with fast path for literals and variables - ZERO-COPY
60    #[inline]
61    pub(super) fn resolve_column_name(&self, col_expr: &CompiledLogic, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
62        match col_expr {
63            // OPTIMIZATION: Direct return for string literals (most common case)
64            CompiledLogic::String(s) => Ok(Value::String(s.clone())),
65            CompiledLogic::Var(name, _) => {
66                // OPTIMIZATION: Check user_data first (column names usually come from there)
67                let value = if name.is_empty() {
68                    helpers::get_var(user_data, name)
69                } else {
70                    helpers::get_var(user_data, name)
71                        .or_else(|| helpers::get_var(internal_context, name))
72                };
73                Ok(value.cloned().unwrap_or(Value::Null))
74            }
75            _ => self.evaluate_with_context(col_expr, user_data, internal_context, depth)
76        }
77    }
78
79    /// Evaluate ValueAt operation - ZERO-COPY with aggressive optimizations
80    pub(super) fn eval_valueat(&self, table_expr: &CompiledLogic, row_idx_expr: &CompiledLogic, col_name_expr: &Option<Box<CompiledLogic>>, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
81        // OPTIMIZATION 1: Try combined single-loop operations first
82        if let Some(result) = self.try_eval_valueat_combined(table_expr, row_idx_expr, col_name_expr, user_data, internal_context, depth)? {
83            return Ok(result);
84        }
85
86        // OPTIMIZATION 2: Fast path for literal row indices (avoid evaluation overhead)
87        let row_idx = match row_idx_expr {
88            CompiledLogic::Number(n) => {
89                // Direct parse for literal numbers
90                match n.parse::<i64>() {
91                    Ok(idx) if idx >= 0 => Some(idx as usize),
92                    _ => None,
93                }
94            }
95            _ => {
96                // Evaluate row index expression
97                let row_idx_val = self.evaluate_with_context(row_idx_expr, user_data, internal_context, depth + 1)?;
98                let row_idx_num = helpers::to_number(&row_idx_val) as i64;
99                if row_idx_num >= 0 {
100                    Some(row_idx_num as usize)
101                } else {
102                    None
103                }
104            }
105        };
106
107        // Early exit if invalid index
108        let row_idx = match row_idx {
109            Some(idx) => idx,
110            None => return Ok(Value::Null),
111        };
112
113        // OPTIMIZATION 3: Resolve table and check bounds together (reduce overhead)
114        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
115        let arr = match table_ref.as_array() {
116            Some(arr) if !arr.is_empty() && row_idx < arr.len() => arr,
117            _ => return Ok(Value::Null),
118        };
119
120        let row = &arr[row_idx];
121
122        // OPTIMIZATION 4: Fast path for column resolution
123        let result = if let Some(col_expr) = col_name_expr {
124            // Try fast path for string literals first
125            let col_name = match col_expr.as_ref() {
126                CompiledLogic::String(s) => s.as_str(),
127                _ => {
128                    // Fall back to full resolution
129                    match self.resolve_column_name(col_expr, user_data, internal_context, depth)? {
130                        Value::String(s) => {
131                            // Need to return cloned value since we can't borrow from temporary
132                            if let Value::Object(obj) = row {
133                                return Ok(obj.get(&s).cloned().unwrap_or(Value::Null));
134                            } else {
135                                return Ok(Value::Null);
136                            }
137                        }
138                        _ => return Ok(Value::Null),
139                    }
140                }
141            };
142            
143            // Direct object field access
144            if let Value::Object(obj) = row {
145                obj.get(col_name).cloned().unwrap_or(Value::Null)
146            } else {
147                Value::Null
148            }
149        } else {
150            row.clone()
151        };
152
153        Ok(result)
154    }
155
156    /// Evaluate MaxAt operation - ZERO-COPY
157    pub(super) fn eval_maxat(&self, table_expr: &CompiledLogic, col_name_expr: &CompiledLogic, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
158        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
159        let col_val = self.resolve_column_name(col_name_expr, user_data, internal_context, depth)?;
160
161        if let Value::String(col_name) = &col_val {
162            let result = if let Some(arr) = table_ref.as_array() {
163                arr.last()
164                    .and_then(|last_row| last_row.as_object())
165                    .and_then(|obj| obj.get(col_name))
166                    .cloned()
167                    .unwrap_or(Value::Null)
168            } else {
169                Value::Null
170            };
171            Ok(result)
172        } else {
173            Ok(Value::Null)
174        }
175    }
176
177    /// Evaluate IndexAt operation - ZERO-COPY
178    pub(super) fn eval_indexat(&self, lookup_expr: &CompiledLogic, table_expr: &CompiledLogic, field_expr: &CompiledLogic, range_expr: &Option<Box<CompiledLogic>>, user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
179        let lookup_val = self.evaluate_with_context(lookup_expr, user_data, internal_context, depth + 1)?;
180        let field_val = self.resolve_column_name(field_expr, user_data, internal_context, depth)?;
181        let field_name = match field_val {
182            Value::String(s) => s,
183            _ => return Ok(self.f64_to_json(-1.0)),
184        };
185
186        let is_range = if let Some(r_expr) = range_expr {
187            let r_val = self.evaluate_with_context(r_expr, user_data, internal_context, depth + 1)?;
188            helpers::is_truthy(&r_val)
189        } else {
190            false
191        };
192
193        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
194        let arr = match table_ref.as_array() {
195            Some(arr) if !arr.is_empty() => arr,
196            _ => return Ok(self.f64_to_json(-1.0)),
197        };
198
199        let lookup_num = if is_range { helpers::to_number(&lookup_val) } else { 0.0 };
200
201        #[cfg(feature = "parallel")]
202        if !is_range && arr.len() >= PARALLEL_THRESHOLD {
203            let result = arr.par_iter()
204                .enumerate()
205                .find_map_first(|(idx, row)| {
206                    if let Value::Object(obj) = row {
207                        if let Some(cell_val) = obj.get(&field_name) {
208                            if helpers::loose_equal(&lookup_val, cell_val) {
209                                return Some(idx as f64);
210                            }
211                        }
212                    }
213                    None
214                });
215            return Ok(self.f64_to_json(result.unwrap_or(-1.0)));
216        }
217        
218        if is_range {
219            for (idx, row) in arr.iter().enumerate() {
220                if let Value::Object(obj) = row {
221                    if let Some(cell_val) = obj.get(&field_name) {
222                        let cell_num = helpers::to_number(cell_val);
223                        if cell_num <= lookup_num {
224                            return Ok(self.f64_to_json(idx as f64));
225                        }
226                    }
227                }
228            }
229            Ok(self.f64_to_json(-1.0))
230        } else {
231            for (idx, row) in arr.iter().enumerate() {
232                if let Value::Object(obj) = row {
233                    if let Some(cell_val) = obj.get(&field_name) {
234                        if helpers::loose_equal(&lookup_val, cell_val) {
235                            return Ok(self.f64_to_json(idx as f64));
236                        }
237                    }
238                }
239            }
240            Ok(self.f64_to_json(-1.0))
241        }
242    }
243
244    /// Evaluate Match operation - ZERO-COPY
245    pub(super) fn eval_match(&self, table_expr: &CompiledLogic, conditions: &[CompiledLogic], user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
246        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
247
248        // Pre-evaluate all condition pairs (value, field) ONCE
249        let mut evaluated_conditions = Vec::with_capacity(conditions.len() / 2);
250        for chunk in conditions.chunks(2) {
251            if chunk.len() == 2 {
252                let value_val = self.evaluate_with_context(&chunk[0], user_data, internal_context, depth + 1)?;
253                let field_val = self.evaluate_with_context(&chunk[1], user_data, internal_context, depth + 1)?;
254                if let Value::String(field) = field_val {
255                    evaluated_conditions.push((value_val, field));
256                }
257            }
258        }
259
260        if let Some(arr) = table_ref.as_array() {
261            #[cfg(feature = "parallel")]
262            if arr.len() >= PARALLEL_THRESHOLD {
263                let result = arr.par_iter()
264                    .enumerate()
265                    .find_map_first(|(idx, row)| {
266                        if let Value::Object(obj) = row {
267                            let all_match = evaluated_conditions.iter().all(|(value_val, field)| {
268                                obj.get(field)
269                                    .map(|cell_val| helpers::loose_equal(value_val, cell_val))
270                                    .unwrap_or(false)
271                            });
272                            if all_match { Some(idx as f64) } else { None }
273                        } else {
274                            None
275                        }
276                    });
277                return Ok(self.f64_to_json(result.unwrap_or(-1.0)));
278            }
279            
280            for (idx, row) in arr.iter().enumerate() {
281                if let Value::Object(obj) = row {
282                    let all_match = evaluated_conditions.iter().all(|(value_val, field)| {
283                        obj.get(field)
284                            .map(|cell_val| helpers::loose_equal(value_val, cell_val))
285                            .unwrap_or(false)
286                    });
287                    if all_match {
288                        return Ok(self.f64_to_json(idx as f64));
289                    }
290                }
291            }
292            Ok(self.f64_to_json(-1.0))
293        } else {
294            Ok(Value::Null)
295        }
296    }
297
298    /// Evaluate MatchRange operation - ZERO-COPY
299    pub(super) fn eval_matchrange(&self, table_expr: &CompiledLogic, conditions: &[CompiledLogic], user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
300        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
301
302        let mut evaluated_conditions = Vec::with_capacity(conditions.len() / 3);
303        for chunk in conditions.chunks(3) {
304            if chunk.len() == 3 {
305                let min_col_val = self.evaluate_with_context(&chunk[0], user_data, internal_context, depth + 1)?;
306                let max_col_val = self.evaluate_with_context(&chunk[1], user_data, internal_context, depth + 1)?;
307                let check_val = self.evaluate_with_context(&chunk[2], user_data, internal_context, depth + 1)?;
308
309                if let (Value::String(min_col), Value::String(max_col)) = (&min_col_val, &max_col_val) {
310                    let check_num = helpers::to_number(&check_val);
311                    evaluated_conditions.push((min_col.clone(), max_col.clone(), check_num));
312                }
313            }
314        }
315
316        if let Some(arr) = table_ref.as_array() {
317            #[cfg(feature = "parallel")]
318            if arr.len() >= PARALLEL_THRESHOLD {
319                let result = arr.par_iter()
320                    .enumerate()
321                    .find_map_first(|(idx, row)| {
322                        if let Value::Object(obj) = row {
323                            let all_match = evaluated_conditions.iter().all(|(min_col, max_col, check_num)| {
324                                let min_num = obj.get(min_col).map(|v| helpers::to_number(v)).unwrap_or(0.0);
325                                let max_num = obj.get(max_col).map(|v| helpers::to_number(v)).unwrap_or(0.0);
326                                *check_num >= min_num && *check_num <= max_num
327                            });
328                            if all_match { Some(idx as f64) } else { None }
329                        } else {
330                            None
331                        }
332                    });
333                return Ok(self.f64_to_json(result.unwrap_or(-1.0)));
334            }
335            
336            for (idx, row) in arr.iter().enumerate() {
337                if let Value::Object(obj) = row {
338                    let all_match = evaluated_conditions.iter().all(|(min_col, max_col, check_num)| {
339                        let min_num = obj.get(min_col).map(|v| helpers::to_number(v)).unwrap_or(0.0);
340                        let max_num = obj.get(max_col).map(|v| helpers::to_number(v)).unwrap_or(0.0);
341                        *check_num >= min_num && *check_num <= max_num
342                    });
343                    if all_match {
344                        return Ok(self.f64_to_json(idx as f64));
345                    }
346                }
347            }
348            Ok(self.f64_to_json(-1.0))
349        } else {
350            Ok(Value::Null)
351        }
352    }
353
354    /// Evaluate Choose operation - ZERO-COPY
355    pub(super) fn eval_choose(&self, table_expr: &CompiledLogic, conditions: &[CompiledLogic], user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
356        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
357
358        let mut evaluated_conditions = Vec::with_capacity(conditions.len() / 2);
359        for chunk in conditions.chunks(2) {
360            if chunk.len() == 2 {
361                let value_val = self.evaluate_with_context(&chunk[0], user_data, internal_context, depth + 1)?;
362                let field_val = self.evaluate_with_context(&chunk[1], user_data, internal_context, depth + 1)?;
363                if let Value::String(field) = field_val {
364                    evaluated_conditions.push((value_val, field));
365                }
366            }
367        }
368
369        if let Some(arr) = table_ref.as_array() {
370            #[cfg(feature = "parallel")]
371            if arr.len() >= PARALLEL_THRESHOLD {
372                let result = arr.par_iter()
373                    .enumerate()
374                    .find_map_first(|(idx, row)| {
375                        if let Value::Object(obj) = row {
376                            let any_match = evaluated_conditions.iter().any(|(value_val, field)| {
377                                obj.get(field)
378                                    .map(|cell_val| helpers::loose_equal(value_val, cell_val))
379                                    .unwrap_or(false)
380                            });
381                            if any_match { Some(idx as f64) } else { None }
382                        } else {
383                            None
384                        }
385                    });
386                return Ok(self.f64_to_json(result.unwrap_or(-1.0)));
387            }
388            
389            for (idx, row) in arr.iter().enumerate() {
390                if let Value::Object(obj) = row {
391                    let any_match = evaluated_conditions.iter().any(|(value_val, field)| {
392                        obj.get(field)
393                            .map(|cell_val| helpers::loose_equal(value_val, cell_val))
394                            .unwrap_or(false)
395                    });
396                    if any_match {
397                        return Ok(self.f64_to_json(idx as f64));
398                    }
399                }
400            }
401            Ok(self.f64_to_json(-1.0))
402        } else {
403            Ok(Value::Null)
404        }
405    }
406
407    /// Evaluate FindIndex operation - ZERO-COPY
408    pub(super) fn eval_findindex(&self, table_expr: &CompiledLogic, conditions: &[CompiledLogic], user_data: &Value, internal_context: &Value, depth: usize) -> Result<Value, String> {
409        let table_ref = self.get_table_array(table_expr, user_data, internal_context, depth)?;
410        let arr = match table_ref.as_array() {
411            Some(arr) if !arr.is_empty() => arr,
412            _ => return Ok(self.f64_to_json(-1.0)),
413        };
414
415        if conditions.is_empty() {
416            return Ok(self.f64_to_json(0.0));
417        }
418
419        #[cfg(feature = "parallel")]
420        if arr.len() >= PARALLEL_THRESHOLD {
421            let result = arr.par_iter()
422                .enumerate()
423                .find_map_first(|(idx, row)| {
424                    for condition in conditions {
425                        // Use row as primary context, user_data as fallback
426                        match self.evaluate_with_context(condition, row, user_data, depth + 1) {
427                            Ok(result) if helpers::is_truthy(&result) => continue,
428                            _ => return None,
429                        }
430                    }
431                    Some(idx as f64)
432                });
433            return Ok(self.f64_to_json(result.unwrap_or(-1.0)));
434        }
435        
436        for (idx, row) in arr.iter().enumerate() {
437            let mut all_match = true;
438            for condition in conditions {
439                // Use row as primary context, user_data as fallback
440                match self.evaluate_with_context(condition, row, user_data, depth + 1) {
441                    Ok(result) if helpers::is_truthy(&result) => continue,
442                    _ => {
443                        all_match = false;
444                        break;
445                    }
446                }
447            }
448            if all_match {
449                return Ok(self.f64_to_json(idx as f64));
450            }
451        }
452        Ok(self.f64_to_json(-1.0))
453    }
454}