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}