json_eval_rs/
table_evaluate.rs

1use serde_json::{Map, Value};
2use std::mem;
3use crate::eval_data::EvalData;
4use crate::{JSONEval, path_utils};
5use crate::table_metadata::RowMetadata;
6
7/// Sandboxed table evaluation for safe parallel execution
8/// 
9/// All heavy operations (dependency analysis, forward reference checks) are done at parse time.
10/// This function creates an isolated scope to prevent interference between parallel table evaluations.
11/// 
12/// # Parallel Safety
13/// 
14/// This function is designed for safe parallel execution:
15/// - Takes `scope_data` as an immutable reference (read-only parent scope)
16/// - Creates an isolated sandbox (clone) for all table-specific mutations
17/// - All temporary variables (`$iteration`, `$threshold`, column vars) exist only in the sandbox
18/// - The parent `scope_data` remains unchanged, preventing race conditions
19/// - Multiple tables can be evaluated concurrently without interference
20/// 
21/// # Mutation Safety
22/// 
23/// **ALL data mutations go through EvalData methods:**
24/// - `sandbox.set()` - sets field values with version tracking
25/// - `sandbox.push_to_array()` - appends to arrays with version tracking
26/// - `sandbox.get_table_row_mut()` - gets mutable row references (followed by mark_modified)
27/// - `sandbox.mark_modified()` - explicitly marks paths as modified
28/// 
29/// This ensures proper version tracking and mutation safety throughout evaluation.
30/// 
31/// # Sandboxing Strategy
32/// 
33/// 1. Clone `scope_data` to create an isolated sandbox at the start
34/// 2. All evaluations and mutations happen within the sandbox via EvalData methods
35/// 3. Extract the final table array from the sandbox
36/// 4. Sandbox is dropped, discarding all temporary state
37/// 5. Parent scope remains pristine and can be safely shared across threads
38pub fn evaluate_table(
39    lib: &JSONEval,  // Changed to immutable - parallel-safe, only reads metadata and calls engine
40    eval_key: &str,
41    scope_data: &EvalData,  // Now immutable - we read from parent scope
42) -> Result<Vec<Value>, String> {
43    // Clone metadata (cheap since it uses Arc internally)
44    let metadata = lib.table_metadata.get(eval_key)
45        .ok_or_else(|| format!("Table metadata not found for {}", eval_key))?.clone();
46    
47    // Pre-compute table path once using JSON pointer format
48    let table_pointer_path = path_utils::normalize_to_json_pointer(eval_key);
49    
50    // ==========================================
51    // CREATE SANDBOXED SCOPE (thread-safe isolation)
52    // ==========================================
53    // Clone scope_data to create an isolated sandbox for this table evaluation
54    // This prevents parallel table evaluations from interfering with each other
55    let mut sandbox = scope_data.clone();
56    
57    // ==========================================
58    // PHASE 0: Evaluate $datas FIRST (before skip/clear)
59    // ==========================================
60    // Capture existing table value and track if dependencies change
61    let existing_table_value = sandbox.get(&table_pointer_path).cloned();
62    
63    // Use empty internal context for $data evaluation
64    let empty_context = Value::Object(Map::new());
65    for (name, logic, literal) in metadata.data_plans.iter() {
66        let value = match logic {
67            Some(logic_id) => match lib.engine.run_with_context(logic_id, sandbox.data(), &empty_context) {
68                Ok(val) => val,
69                Err(_) => literal.as_ref().map(|arc_val| Value::clone(arc_val)).unwrap_or(Value::Null),
70            },
71            None => literal.as_ref().map(|arc_val| Value::clone(arc_val)).unwrap_or(Value::Null),
72        };
73        
74        sandbox.set(name.as_ref(), value);
75    }
76
77    // ==========================================
78    // PHASE 1: Evaluate $skip - if true, return empty immediately
79    // ==========================================
80    let mut should_skip = metadata.skip_literal;
81    if !should_skip {
82        if let Some(logic_id) = metadata.skip_logic {
83            let val = lib.engine.run_with_context(&logic_id, sandbox.data(), &empty_context)?;
84            should_skip = val.as_bool().unwrap_or(false);
85        }
86    }
87
88    // ==========================================
89    // PHASE 2: Check dependencies before evaluation
90    // ==========================================
91    // Skip evaluation if required dependencies (non-$params, non-$ prefixed) are null/empty
92    let mut requirement_not_filled = false;
93    if let Some(deps) = lib.dependencies.get(eval_key) {
94        for dep in deps.iter() {
95            // Skip $params and any dependency starting with $
96            if dep.contains("$params") || (!dep.contains("$context") && (dep.starts_with("/$") || dep.starts_with("$"))) {
97                continue;
98            }
99            
100            // Check if this dependency is null or empty
101            if let Some(dep_value) = sandbox.get_without_properties(dep) {
102                let is_empty = match dep_value {
103                    Value::Null => true,
104                    Value::String(s) => s.is_empty(),
105                    Value::Array(arr) => arr.is_empty(),
106                    Value::Object(obj) => obj.is_empty(),
107                    _ => false,
108                };
109                
110                if is_empty {
111                    // println!("dependency {} is empty", dep);
112                    requirement_not_filled = true;
113                    break;
114                }
115            } else {
116                // println!("dependency {} doesn't exist", dep);
117                // Dependency doesn't exist, treat as null
118                requirement_not_filled = true;
119                break;
120            }
121        }
122    }
123    // println!("requirement_not_filled: {}", requirement_not_filled);
124
125    // ==========================================
126    // PHASE 3: Evaluate $clear - if true, ensure table is empty
127    // ==========================================
128    let mut should_clear = metadata.clear_literal;
129    if !should_clear {
130        if let Some(logic_id) = metadata.clear_logic {
131            let val = lib.engine.run_with_context(&logic_id, sandbox.data(), &empty_context)?;
132            should_clear = val.as_bool().unwrap_or(false);
133        }
134    }
135
136    // Initialize empty table array only when: existing table data is not an array
137    let table_is_not_array = !existing_table_value.as_ref().map_or(false, |v| v.is_array());
138    if should_clear || should_skip  || table_is_not_array || requirement_not_filled {
139        sandbox.set(&table_pointer_path, Value::Array(Vec::new()));
140    }
141
142    if should_clear || should_skip || requirement_not_filled {
143        return Ok(Vec::new());
144    }
145    
146    let number_from_value = |value: &Value| -> i64 {
147        match value {
148            Value::Number(n) => n.as_i64().unwrap_or_else(|| n.as_f64().map_or(0, |f| f as i64)),
149            Value::String(s) => s.parse::<f64>().map_or(0, |f| f as i64),
150            Value::Bool(true) => 1,
151            Value::Bool(false) => 0,
152            _ => 0,
153        }
154    };
155
156    for plan in metadata.row_plans.iter() {
157        match plan {
158            RowMetadata::Static { columns } => {
159                // CRITICAL: Preserve SCHEMA ORDER for static rows (match JavaScript behavior)
160                let mut evaluated_row = Map::with_capacity(columns.len());
161                
162                // Create internal context for column variables
163                let mut internal_context = Map::new();
164                
165                // Evaluate columns in schema order (sandboxed)
166                for column in columns.iter() {
167                    let value = if let Some(logic_id) = column.logic {
168                        lib.engine.run_with_context(&logic_id, sandbox.data(), &Value::Object(internal_context.clone()))?
169                    } else {
170                        column.literal.as_ref().map(|arc_val| Value::clone(arc_val)).unwrap_or(Value::Null)
171                    };
172                    // Pre-compute string key once from Arc<str>
173                    let col_name_str = column.name.as_ref().to_string();
174                    // Store in internal context (column vars start with $)
175                    internal_context.insert(column.var_path.as_ref().to_string(), value.clone());
176                    evaluated_row.insert(col_name_str, value);
177                }
178                
179                sandbox.push_to_array(&table_pointer_path, Value::Object(evaluated_row));
180            }
181            RowMetadata::Repeat {
182                start,
183                end,
184                columns,
185                forward_cols,
186                normal_cols,
187            } => {
188                // Evaluate repeat bounds in sandbox
189                let start_val = if let Some(logic_id) = start.logic {
190                    lib.engine.run_with_context(&logic_id, sandbox.data(), &empty_context)?
191                } else {
192                    Value::clone(&start.literal)
193                };
194                let end_val = if let Some(logic_id) = end.logic {
195                    lib.engine.run_with_context(&logic_id, sandbox.data(), &empty_context)?
196                } else {
197                    Value::clone(&end.literal)
198                };
199
200                let start_idx = number_from_value(&start_val);
201                let end_idx = number_from_value(&end_val);
202
203                if start_idx > end_idx {
204                    continue;
205                }
206                
207                // Count existing static rows in sandbox
208                let existing_row_count = sandbox
209                    .get(&table_pointer_path)
210                    .and_then(|v| v.as_array())
211                    .map(|arr| arr.len())
212                    .unwrap_or(0);
213
214                // Pre-allocate all rows in sandbox (zero-copy: pre-compute string keys)
215                let total_rows = (end_idx - start_idx + 1) as usize;
216                let col_count = columns.len();
217                // Pre-compute all column name strings once
218                let col_names: Vec<String> = columns.iter()
219                    .map(|col| col.name.as_ref().to_string())
220                    .collect();
221                
222                if let Some(Value::Array(table_arr)) = sandbox.get_mut(&table_pointer_path) {
223                    table_arr.reserve(total_rows);
224                    for _ in 0..total_rows {
225                        let mut row = Map::with_capacity(col_count);
226                        for col_name in &col_names {
227                            row.insert(col_name.clone(), Value::Null);
228                        }
229                        table_arr.push(Value::Object(row));
230                    }
231                }
232                
233                // ========================================
234                // PHASE 4: TOP TO BOTTOM (Forward Pass)
235                // ========================================
236                // Evaluate columns WITHOUT forward references in sandbox
237                
238                // Create internal context with $threshold
239                let mut internal_context = Map::new();
240                internal_context.insert("$threshold".to_string(), Value::from(end_idx));
241                
242                for iteration in start_idx..=end_idx {
243                    let row_idx = (iteration - start_idx) as usize;
244                    let target_idx = existing_row_count + row_idx;
245                    
246                    // Set $iteration in internal context
247                    internal_context.insert("$iteration".to_string(), Value::from(iteration));
248                    
249                    // Evaluate normal columns in sandbox
250                    for &col_idx in normal_cols.iter() {
251                        let column = &columns[col_idx];
252                        let value = match column.logic {
253                            Some(logic_id) => lib.engine.run_with_context(&logic_id, sandbox.data(), &Value::Object(internal_context.clone()))?,
254                            None => column.literal.as_ref().map(|arc_val| Value::clone(arc_val)).unwrap_or(Value::Null),
255                        };
256                        
257                        // Update table cell in sandbox
258                        if let Some(row_obj) = sandbox.get_table_row_mut(&table_pointer_path, target_idx) {
259                            if let Some(cell) = row_obj.get_mut(column.name.as_ref()) {
260                                *cell = value.clone();
261                            } else {
262                                row_obj.insert(col_names[col_idx].clone(), value.clone());
263                            }
264                        }
265                        // Store in internal context (column vars)
266                        internal_context.insert(column.var_path.as_ref().to_string(), value);
267                    }
268                }
269                // TODO: Implement mark_modified if needed for tracking
270                // sandbox.mark_modified(&table_pointer_path);
271                
272                // ========================================
273                // PHASE 5 (BACKWARD PASS):
274                // Evaluate columns WITH forward references in sandbox
275                // ========================================
276                if !forward_cols.is_empty() {
277                    let max_sweeps = 100; // Safety limit to prevent infinite loops
278                    let mut scan_from_down = false;
279                    let iter_count = (end_idx - start_idx + 1) as usize;
280                    
281                    // Create internal context for backward pass
282                    let mut internal_context = Map::new();
283                    internal_context.insert("$threshold".to_string(), Value::from(end_idx));
284                    
285                    // Track which columns changed in previous sweep per row
286                    // This enables skipping re-evaluation of columns with unchanged dependencies
287                    let mut prev_changed: Vec<Vec<bool>> = vec![vec![true; forward_cols.len()]; iter_count];
288
289                    for _sweep_num in 1..=max_sweeps {
290                        let mut any_changed = false;
291                        let mut curr_changed: Vec<Vec<bool>> = vec![vec![false; forward_cols.len()]; iter_count];
292                        
293                        for iter_offset in 0..iter_count {
294                            let iteration = if scan_from_down {
295                                end_idx - iter_offset as i64
296                            } else {
297                                start_idx + iter_offset as i64
298                            };
299                            let row_offset = (iteration - start_idx) as usize;
300                            let target_idx = existing_row_count + row_offset;
301                            
302                            // Set $iteration in internal context
303                            internal_context.insert("$iteration".to_string(), Value::from(iteration));
304                            
305                            // Restore column values from sandbox to internal context
306                            if let Some(Value::Array(table_arr)) = sandbox.get(&table_pointer_path) {
307                                if let Some(Value::Object(row_obj)) = table_arr.get(target_idx) {
308                                    // Collect all column values into internal context
309                                    for &col_idx in normal_cols.iter().chain(forward_cols.iter()) {
310                                        let column = &columns[col_idx];
311                                        if let Some(value) = row_obj.get(column.name.as_ref()) {
312                                            internal_context.insert(column.var_path.as_ref().to_string(), value.clone());
313                                        }
314                                    }
315                                }
316                            }
317                            
318                            // Evaluate forward columns in sandbox (with dependency-aware skipping)
319                            for (fwd_idx, &col_idx) in forward_cols.iter().enumerate() {
320                                let column = &columns[col_idx];
321                                
322                                // Determine if we should evaluate this column
323                                let mut should_evaluate = _sweep_num == 1; // Always evaluate first sweep
324                                
325                                // Skip if no dependencies changed (only for non-forward-ref columns)
326                                if !should_evaluate && !column.has_forward_ref {
327                                    // Check intra-row column dependencies
328                                    should_evaluate = column.dependencies.iter().any(|dep| {
329                                        if dep.starts_with('$') {
330                                            let dep_name = dep.trim_start_matches('$');
331                                            // Check if dependency is in forward_cols and changed in prev sweep
332                                            forward_cols.iter().enumerate().any(|(dep_fwd_idx, &dep_col_idx)| {
333                                                columns[dep_col_idx].name.as_ref() == dep_name 
334                                                    && prev_changed[row_offset][dep_fwd_idx]
335                                            })
336                                        } else {
337                                            // Non-column dependency, always re-evaluate to be safe
338                                            true
339                                        }
340                                    });
341                                } else if !should_evaluate {
342                                    // For forward-ref columns, re-evaluate if anything changed
343                                    should_evaluate = true;
344                                }
345                                
346                                if should_evaluate {
347                                    let value = match column.logic {
348                                        Some(logic_id) => lib.engine.run_with_context(&logic_id, sandbox.data(), &Value::Object(internal_context.clone()))?,
349                                        None => column.literal.as_ref().map(|arc_val| Value::clone(arc_val)).unwrap_or(Value::Null),
350                                    };
351                                    
352                                    // Write to sandbox table and update internal context
353                                    if let Some(row_obj) = sandbox.get_table_row_mut(&table_pointer_path, target_idx) {
354                                        if let Some(cell) = row_obj.get_mut(column.name.as_ref()) {
355                                            if *cell != value {
356                                                any_changed = true;
357                                                curr_changed[row_offset][fwd_idx] = true;
358                                                *cell = value.clone();
359                                            }
360                                        } else {
361                                            any_changed = true;
362                                            curr_changed[row_offset][fwd_idx] = true;
363                                            row_obj.insert(col_names[col_idx].clone(), value.clone());
364                                        }
365                                    }
366                                    // Update internal context with new value
367                                    internal_context.insert(column.var_path.as_ref().to_string(), value);
368                                }
369                            }
370                        }
371                        
372                        scan_from_down = !scan_from_down;
373                        prev_changed = curr_changed;
374                        
375                        // Exit early if converged (no changes in this sweep)
376                        if !any_changed {
377                            break;
378                        }
379                    }
380                }
381            }
382        }
383    }
384
385    // Extract result from sandbox (zero-copy: take from sandbox, no mutation of parent scope)
386    let final_rows = if let Some(table_value) = sandbox.get_mut(&table_pointer_path) {
387        if let Some(array) = table_value.as_array_mut() {
388            mem::take(array)
389        } else {
390            Vec::new()
391        }
392    } else {
393        Vec::new()
394    };
395
396    // Sandbox is dropped here, all temporary mutations are discarded
397    // Parent scope_data remains unchanged - safe for parallel execution
398    Ok(final_rows)
399}