Skip to main content

json_eval_rs/jsoneval/
table_evaluate.rs

1use crate::jsoneval::eval_data::EvalData;
2use crate::jsoneval::path_utils;
3use crate::jsoneval::table_metadata::RowMetadata;
4use crate::time_block;
5use crate::JSONEval;
6use serde_json::{Map, Value};
7use std::collections::{HashMap, HashSet};
8
9use crate::jsoneval::cancellation::CancellationToken;
10
11/// Zero-sandbox table evaluation
12///
13/// Eliminates the full `EvalData` clone (sandbox) by:
14/// 1. **Local row storage**: rows are built into a `Vec<Value>` directly on the stack.
15/// 2. **Self-table scope**: the evaluator's `TableScope` intercepts Var/Ref/ValueAt
16///    lookups for the current table's path, returning rows from local storage.
17/// 3. **Direct mutation**: forward/backward passes index `local_rows` by integer —
18///    no `Arc::make_mut`, no JSON-pointer traversal per cell.
19/// 4. **$datas in context**: evaluated variable bindings are passed as entries
20///    inside `internal_context` (checked first), not written to scope_data.
21///
22/// The caller (`evaluate_internal`) remains responsible for writing results back
23/// to `eval_data` / `static_arrays` / `evaluated_schema`.
24pub fn evaluate_table(
25    lib: &JSONEval,
26    eval_key: &str,
27    scope_data: &EvalData,
28    token: Option<&CancellationToken>,
29) -> Result<(std::sync::Arc<Value>, Option<indexmap::IndexSet<String>>), String> {
30    let _total_start: Option<std::time::Instant> = if crate::utils::is_timing_enabled() {
31        Some(std::time::Instant::now())
32    } else {
33        None
34    };
35    let result = evaluate_table_inner(lib, eval_key, scope_data, token);
36    if let Some(start) = _total_start {
37        crate::utils::record_timing(&format!("[table::{}] total", eval_key), start.elapsed());
38    }
39    result
40}
41
42#[inline(always)]
43fn is_pure_arithmetic(logic: &crate::rlogic::CompiledLogic) -> bool {
44    matches!(
45        logic,
46        crate::rlogic::CompiledLogic::Add(_)
47            | crate::rlogic::CompiledLogic::Subtract(_)
48            | crate::rlogic::CompiledLogic::Multiply(_)
49            | crate::rlogic::CompiledLogic::Divide(_)
50            | crate::rlogic::CompiledLogic::Modulo(_, _)
51            | crate::rlogic::CompiledLogic::Power(_, _)
52            | crate::rlogic::CompiledLogic::Round(_, _)
53            | crate::rlogic::CompiledLogic::RoundUp(_, _)
54            | crate::rlogic::CompiledLogic::RoundDown(_, _)
55            | crate::rlogic::CompiledLogic::Abs(_)
56    )
57}
58
59fn evaluate_table_inner(
60    lib: &JSONEval,
61    eval_key: &str,
62    scope_data: &EvalData,
63    token: Option<&CancellationToken>,
64) -> Result<(std::sync::Arc<Value>, Option<indexmap::IndexSet<String>>), String> {
65    let metadata = lib
66        .table_metadata
67        .get(eval_key)
68        .ok_or_else(|| format!("Table metadata not found for {}", eval_key))?
69        .clone();
70
71    if let Some(t) = token {
72        if t.is_cancelled() {
73            return Err("Cancelled".to_string());
74        }
75    }
76
77    let table_pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
78
79    let mut external_deps = indexmap::IndexSet::new();
80    let pointer_data_prefix =
81        crate::jsoneval::path_utils::schema_path_to_data_pointer(&table_pointer_path).into_owned();
82    let pointer_data_prefix_slash = format!("{}/", pointer_data_prefix);
83
84    if let Some(deps) = lib.dependencies.get(eval_key) {
85        for dep in deps {
86            let is_params_dep = dep.contains("$params");
87            let is_other_system_dep = !is_params_dep
88                && !dep.contains("$context")
89                && (dep.starts_with("/$") || dep.starts_with("$"));
90
91            if is_other_system_dep {
92                continue;
93            }
94
95            let dep_data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(dep);
96            if dep_data_path == pointer_data_prefix
97                || dep_data_path.starts_with(&pointer_data_prefix_slash)
98            {
99                continue;
100            }
101
102            external_deps.insert(dep.clone());
103        }
104    }
105
106    if crate::utils::is_debug_cache_enabled() && external_deps.is_empty() {
107        if !metadata.data_plans.is_empty() {
108            eprintln!(
109                "[jsoneval DEBUG] table {} has zero external_deps but \
110                 non-empty data_plans — $params changes may not \
111                 invalidate its cache",
112                eval_key
113            );
114        }
115    }
116
117    if let Some(cached_result) = lib.eval_cache.check_table_cache(eval_key, &external_deps) {
118        if cached_result.is_array() {
119            return Ok((cached_result, None)); // Signal that we had a cache hit
120        }
121    }
122
123    // PHASE 0: Evaluate $datas first.
124    // Instead of writing to a sandbox, we collect overrides into `data_ctx` which
125    // gets merged into ctx_value (internal_context). The evaluator checks
126    // internal_context before user_data, so $datas are visible to all column logic.
127    let mut data_ctx: Map<String, Value> = Map::new();
128    time_block!(&format!("[table::{}] phase0 $datas", eval_key), {
129        let empty_ctx = Value::Object(Map::new());
130        for (name, logic, literal) in metadata.data_plans.iter() {
131            let value = match logic {
132                Some(logic_id) => {
133                    match lib
134                        .engine
135                        .run_with_context(logic_id, scope_data.data(), &empty_ctx)
136                    {
137                        Ok(val) => val,
138                        Err(_) => literal
139                            .as_ref()
140                            .map(|arc_val| Value::clone(arc_val))
141                            .unwrap_or(Value::Null),
142                    }
143                }
144                None => literal
145                    .as_ref()
146                    .map(|arc_val| Value::clone(arc_val))
147                    .unwrap_or(Value::Null),
148            };
149
150            let key = name.as_ref().trim_start_matches('/').to_string();
151            data_ctx.insert(key, value);
152        }
153    });
154
155    // PHASE 1: Evaluate $skip
156    let mut should_skip = metadata.skip_literal;
157    if !should_skip {
158        if let Some(logic_id) = metadata.skip_logic {
159            let ctx = Value::Object(data_ctx.clone());
160            let val = time_block!(&format!("[table::{}] phase1 $skip", eval_key), {
161                lib.engine
162                    .run_with_context(&logic_id, scope_data.data(), &ctx)
163                    .unwrap_or(Value::Null)
164            });
165            should_skip = val.as_bool().unwrap_or(false);
166        }
167    }
168
169    // PHASE 2: Check dependencies
170    let mut requirement_not_filled = false;
171    time_block!(&format!("[table::{}] phase2 dep-check", eval_key), {
172        if let Some(deps) = lib.dependencies.get(eval_key) {
173            for dep in deps.iter() {
174                let is_params_dep = dep.contains("$params");
175                let is_other_system_dep = !is_params_dep
176                    && !dep.contains("$context")
177                    && (dep.starts_with("/$") || dep.starts_with("$"));
178
179                if is_other_system_dep || is_params_dep {
180                    continue;
181                }
182
183                // Validate the dep's current value against its schema rules on-demand.
184                // If the value is absent from scope_data (Null), skip validation entirely —
185                // the dep belongs to a different evaluation context (e.g., a subform path
186                // like /subform/field evaluated during main-form context). Treating
187                // an absent dep as a required-rule failure causes spurious cache misses.
188                let dep_value = scope_data.get_without_properties(dep);
189                let dep_value = match dep_value {
190                    Some(v) if *v != Value::Null => v,
191                    _ => continue,
192                };
193
194                if lib.dep_fails_schema_rules(dep, dep_value, scope_data.data()) {
195                    if crate::utils::is_debug_cache_enabled() {
196                        println!(
197                            "Table Cache MISS [table::{}] dep {} fails schema rules",
198                            eval_key, dep
199                        );
200                    }
201                    requirement_not_filled = true;
202                    break;
203                }
204            }
205        }
206    });
207
208    // PHASE 3: Evaluate $clear
209    let mut should_clear = metadata.clear_literal;
210    if !should_clear {
211        if let Some(logic_id) = metadata.clear_logic {
212            let ctx = Value::Object(data_ctx.clone());
213            let val = time_block!(&format!("[table::{}] phase3 $clear", eval_key), {
214                lib.engine
215                    .run_with_context(&logic_id, scope_data.data(), &ctx)
216                    .unwrap_or(Value::Null)
217            });
218            should_clear = val.as_bool().unwrap_or(false);
219        }
220    }
221
222    if should_clear || should_skip || requirement_not_filled {
223        if crate::utils::is_debug_cache_enabled() {
224            println!("Table Cache MISS [table::{}] should_clear={}, should_skip={}, requirement_not_filled={} (external_deps={:?})", eval_key, should_clear, should_skip, requirement_not_filled, external_deps);
225        }
226        return Ok((
227            std::sync::Arc::new(Value::Array(Vec::new())),
228            Some(external_deps),
229        ));
230    }
231
232    let number_from_value = |value: &Value| -> i64 {
233        match value {
234            Value::Number(n) => n
235                .as_i64()
236                .unwrap_or_else(|| n.as_f64().map_or(0, |f| f as i64)),
237            Value::String(s) => s.parse::<f64>().map_or(0, |f| f as i64),
238            Value::Bool(true) => 1,
239            Value::Bool(false) => 0,
240            _ => 0,
241        }
242    };
243
244    // Accumulate all row plans into a single local_rows Vec
245    let mut local_rows: Vec<Value> = Vec::new();
246
247    for plan in metadata.row_plans.iter() {
248        match plan {
249            RowMetadata::Static { columns } => {
250                time_block!(&format!("[table::{}] static-row", eval_key), {
251                    let mut evaluated_row = Map::with_capacity(columns.len());
252                    let mut ctx_value = Value::Object(data_ctx.clone());
253
254                    for column in columns.iter() {
255                        let value = if let Some(logic_id) = column.logic {
256                            lib.engine
257                                .run_with_context(&logic_id, scope_data.data(), &ctx_value)
258                                .unwrap_or(Value::Null)
259                        } else {
260                            column
261                                .literal
262                                .as_ref()
263                                .map(|arc_val| Value::clone(arc_val))
264                                .unwrap_or(Value::Null)
265                        };
266
267                        if let Value::Object(ref mut map) = ctx_value {
268                            map.insert(column.var_path.as_ref().to_string(), value.clone());
269                        }
270                        evaluated_row.insert(column.name.as_ref().to_string(), value);
271                    }
272
273                    local_rows.push(Value::Object(evaluated_row));
274                });
275            }
276            RowMetadata::Repeat {
277                start,
278                end,
279                columns,
280                forward_cols,
281                normal_cols,
282            } => {
283                let empty_ctx = Value::Object(data_ctx.clone());
284
285                let start_val = if let Some(logic_id) = start.logic {
286                    match lib
287                        .engine
288                        .run_with_context(&logic_id, scope_data.data(), &empty_ctx)
289                    {
290                        Ok(v) => v,
291                        Err(_) => {
292                            // Logic failed: try to use literal as a number, else skip this row group
293                            if let Some(n) = start.literal.as_i64() {
294                                Value::from(n)
295                            } else {
296                                continue; // can't determine bounds, skip
297                            }
298                        }
299                    }
300                } else {
301                    Value::clone(&start.literal)
302                };
303                let end_val = if let Some(logic_id) = end.logic {
304                    match lib
305                        .engine
306                        .run_with_context(&logic_id, scope_data.data(), &empty_ctx)
307                    {
308                        Ok(v) => v,
309                        Err(_) => {
310                            if let Some(n) = end.literal.as_i64() {
311                                Value::from(n)
312                            } else {
313                                continue;
314                            }
315                        }
316                    }
317                } else {
318                    Value::clone(&end.literal)
319                };
320
321                let start_idx = number_from_value(&start_val);
322                let end_idx = number_from_value(&end_val);
323
324                if start_idx > end_idx {
325                    continue;
326                }
327
328                let existing_row_count = local_rows.len();
329                let total_rows = (end_idx - start_idx + 1) as usize;
330                let col_count = columns.len();
331                let _ = col_count;
332
333                // Pre-compute column name strings once
334                let col_names: Vec<String> = columns
335                    .iter()
336                    .map(|col| col.name.as_ref().to_string())
337                    .collect();
338
339                let mut col_map = rapidhash::RapidHashMap::default();
340                for (i, col) in columns.iter().enumerate() {
341                    col_map.insert(col.name.as_ref().to_string(), i);
342                }
343
344                // Build base ctx with data_ctx entries + iteration slots
345                let key_iteration = String::from("$iteration");
346                let key_threshold = String::from("$threshold");
347                let threshold_value = Value::from(end_idx);
348
349                let mut ctx_value = Value::Object({
350                    let mut m = data_ctx.clone();
351                    m.insert(key_threshold.clone(), threshold_value.clone());
352                    m.insert(key_iteration.clone(), Value::Null);
353                    m
354                });
355
356                // Pre-resolve compiled logic references with Loop Invariant Code Motion (LICM)
357                let table_no_hash = table_pointer_path.trim_start_matches('#');
358                let folded_col_logics: Vec<Option<crate::rlogic::CompiledLogic>> = columns
359                    .iter()
360                    .map(|col| {
361                        col.logic.as_ref().and_then(|id| {
362                            lib.engine.get_compiled(id).map(|ast| {
363                                ast.fold_table_invariants(
364                                    lib.engine.evaluator(),
365                                    scope_data.data(),
366                                    &ctx_value,
367                                    &table_pointer_path,
368                                    table_no_hash,
369                                )
370                            })
371                        })
372                    })
373                    .collect();
374                let col_logics: Vec<Option<&crate::rlogic::CompiledLogic>> =
375                    folded_col_logics.iter().map(|opt| opt.as_ref()).collect();
376
377                let col_bytecodes: Vec<Option<crate::rlogic::TableBytecode>> = folded_col_logics
378                    .iter()
379                    .map(|opt| {
380                        opt.as_ref().and_then(|ast| {
381                            crate::rlogic::try_lower_to_bytecode(
382                                ast,
383                                &table_pointer_path,
384                                table_no_hash,
385                                &col_map,
386                            )
387                        })
388                    })
389                    .collect();
390
391                // Pre-allocate flat cells buffer with null cells (1 single contiguous allocation)
392                let mut flat_cells = vec![Value::Null; total_rows * col_count];
393
394                // Register this table's scope on the evaluator so self-table
395                // Var/Ref/ValueAt lookups resolve from flat_cells / local_rows.
396                // The guard is dropped at end of this block, clearing the scope.
397                let _scope_guard = lib
398                    .engine
399                    .enter_table_scope(table_pointer_path.clone(), &local_rows);
400
401                lib.engine.set_table_scope_flat_cells(
402                    flat_cells.as_mut_ptr(),
403                    col_count,
404                    total_rows,
405                    existing_row_count,
406                    col_map,
407                );
408
409                lib.engine.set_table_scope_threshold(end_idx);
410
411                // PHASE 4: FORWARD PASS — top to bottom
412                time_block!(
413                    &format!("[table::{}] forward-pass rows={}", eval_key, total_rows),
414                    {
415                        for iteration in start_idx..=end_idx {
416                            if let Some(t) = token {
417                                if t.is_cancelled() {
418                                    return Err("Cancelled".to_string());
419                                }
420                            }
421                            let row_offset = (iteration - start_idx) as usize;
422                            let row_idx = existing_row_count + row_offset;
423
424                            // Update $iteration in ctx_value in-place
425                            if let Value::Object(ref mut map) = ctx_value {
426                                if let Some(slot) = map.get_mut(&key_iteration) {
427                                    *slot = Value::from(iteration);
428                                }
429                            }
430
431                            // Point get_var lookup directly to the actively evaluating cell
432                            lib.engine
433                                .set_table_scope_cursor(Some(row_idx), Some(iteration));
434
435                            let row_base = row_offset * col_count;
436                            let flat_cells_ptr = flat_cells.as_ptr();
437                            let static_rows_ptr = &local_rows as *const Vec<Value>;
438
439                            for &col_idx in normal_cols.iter() {
440                                let column = &columns[col_idx];
441                                if let Some(ref bc) = col_bytecodes[col_idx] {
442                                    let num = unsafe {
443                                        bc.execute(
444                                            flat_cells_ptr,
445                                            col_count,
446                                            row_offset,
447                                            existing_row_count,
448                                            total_rows,
449                                            iteration,
450                                            static_rows_ptr,
451                                            &col_names,
452                                        )
453                                    };
454                                    if let Some(n) = num {
455                                        flat_cells[row_base + col_idx] = lib.engine.f64_to_value(n);
456                                        continue;
457                                    }
458                                }
459
460                                let value = match col_logics[col_idx] {
461                                    Some(compiled) => {
462                                        if is_pure_arithmetic(compiled) {
463                                            if let Ok(Some(num)) =
464                                                lib.engine.run_precompiled_f64_with_context(
465                                                    compiled,
466                                                    scope_data.data(),
467                                                    &ctx_value,
468                                                )
469                                            {
470                                                lib.engine.f64_to_value(num)
471                                            } else {
472                                                lib.engine
473                                                    .run_precompiled_with_context(
474                                                        compiled,
475                                                        scope_data.data(),
476                                                        &ctx_value,
477                                                    )
478                                                    .unwrap_or(Value::Null)
479                                            }
480                                        } else {
481                                            lib.engine
482                                                .run_precompiled_with_context(
483                                                    compiled,
484                                                    scope_data.data(),
485                                                    &ctx_value,
486                                                )
487                                                .unwrap_or(Value::Null)
488                                        }
489                                    }
490                                    None => column
491                                        .literal
492                                        .as_ref()
493                                        .map(|arc_val| Value::clone(arc_val))
494                                        .unwrap_or(Value::Null),
495                                };
496
497                                // Write directly into flat_cells — no string hash, no IndexMap search
498                                flat_cells[row_base + col_idx] = value;
499                            }
500                            // Reset cursor after row
501                            lib.engine.set_table_scope_cursor(None, None);
502                        }
503                    }
504                );
505
506                // PHASE 5: BACKWARD PASS for forward-ref columns
507                if !forward_cols.is_empty() {
508                    let max_sweeps = 100;
509                    let mut scan_from_down = true;
510                    let iter_count = (end_idx - start_idx + 1) as usize;
511
512                    // Build backward-pass ctx_value (same structure as forward)
513                    let mut ctx_value = Value::Object({
514                        let mut m = data_ctx.clone();
515                        m.insert(key_threshold.clone(), threshold_value.clone());
516                        m.insert(key_iteration.clone(), Value::Null);
517                        m
518                    });
519
520                    // [Opt 4] Pre-compute HashMap/HashSet for O(1) dep lookups
521                    let forward_col_map: HashMap<&str, usize> = forward_cols
522                        .iter()
523                        .enumerate()
524                        .map(|(fwd_idx, &col_idx)| (columns[col_idx].name.as_ref(), fwd_idx))
525                        .collect();
526                    let normal_col_set: HashSet<&str> = normal_cols
527                        .iter()
528                        .map(|&col_idx| columns[col_idx].name.as_ref())
529                        .collect();
530
531                    // Pre-compute backward dependency mappings to integer arrays preventing string sweeps
532                    let table_name_only = table_pointer_path.rsplit('/').next().unwrap_or("");
533                    let mut unknown_deps = vec![false; forward_cols.len()];
534                    let forward_deps: Vec<Vec<usize>> = forward_cols
535                        .iter()
536                        .enumerate()
537                        .map(|(fwd_idx, &col_idx)| {
538                            let mut deps = Vec::new();
539                            for dep in columns[col_idx].dependencies.iter() {
540                                if dep == "$iteration" || dep == "$threshold" {
541                                    continue;
542                                }
543                                let is_self_table = (!table_name_only.is_empty()
544                                    && dep.contains(table_name_only))
545                                    || dep.contains(&table_pointer_path);
546                                if is_self_table {
547                                    unknown_deps[fwd_idx] = true;
548                                    continue;
549                                }
550                                if dep.starts_with('$') {
551                                    let dep_name = dep.trim_start_matches('$');
552                                    if let Some(&dep_fwd_idx) = forward_col_map.get(dep_name) {
553                                        deps.push(dep_fwd_idx);
554                                    } else if normal_col_set.contains(dep_name) {
555                                        // Dependency is in normal_cols: normal columns are already evaluated in Phase 4 and invariant during Phase 5
556                                        continue;
557                                    } else if dep_name.starts_with("params")
558                                        || dep_name.starts_with("constants")
559                                        || dep_name.starts_with("datas")
560                                    {
561                                        // External data reference: invariant during table evaluation
562                                        continue;
563                                    } else {
564                                        unknown_deps[fwd_idx] = true;
565                                    }
566                                } else {
567                                    // External schema path (e.g. #/properties/...): invariant during table evaluation
568                                    continue;
569                                }
570                            }
571                            deps
572                        })
573                        .collect();
574
575                    let changed_len = iter_count * forward_cols.len();
576                    let mut prev_changed = vec![true; changed_len];
577                    let mut curr_changed = vec![false; changed_len];
578
579                    let _backward_start: Option<std::time::Instant> =
580                        if crate::utils::is_timing_enabled() {
581                            Some(std::time::Instant::now())
582                        } else {
583                            None
584                        };
585                    let mut total_sweeps: usize = 0;
586
587                    for _sweep_num in 1..=max_sweeps {
588                        total_sweeps = _sweep_num;
589                        let mut any_changed = false;
590                        let _sweep_step_start = if crate::utils::is_timing_enabled() {
591                            Some(std::time::Instant::now())
592                        } else {
593                            None
594                        };
595                        curr_changed.fill(false);
596
597                        // Update scope so all rows are visible during backward sweep
598                        lib.engine.update_table_scope_rows(&local_rows);
599
600                        for iter_offset in 0..iter_count {
601                            if let Some(t) = token {
602                                if t.is_cancelled() {
603                                    return Err("Cancelled".to_string());
604                                }
605                            }
606                            let iteration = if scan_from_down {
607                                end_idx - iter_offset as i64
608                            } else {
609                                start_idx + iter_offset as i64
610                            };
611                            let row_offset = (iteration - start_idx) as usize;
612                            let target_idx = existing_row_count + row_offset;
613
614                            // Update $iteration in ctx_value in-place
615                            if let Value::Object(ref mut map) = ctx_value {
616                                if let Some(slot) = map.get_mut(&key_iteration) {
617                                    *slot = Value::from(iteration);
618                                }
619                            }
620
621                            // Explicitly direct column resolution to local stack rows cursor and iteration
622                            lib.engine
623                                .set_table_scope_cursor(Some(target_idx), Some(iteration));
624
625                            let row_base = row_offset * col_count;
626                            let fwd_row_base = row_offset * forward_cols.len();
627                            macro_rules! eval_col {
628                                ($fwd_idx:expr, $col_idx:expr) => {{
629                                    let fwd_idx = $fwd_idx;
630                                    let col_idx = $col_idx;
631                                    let column = &columns[col_idx];
632
633                                    let should_evaluate = if _sweep_num == 1 {
634                                        true
635                                    } else if unknown_deps[fwd_idx] {
636                                        true
637                                    } else {
638                                        let deps = &forward_deps[fwd_idx];
639                                        let self_changed = curr_changed
640                                            .get(fwd_row_base + fwd_idx)
641                                            .copied()
642                                            .unwrap_or(false);
643                                        if self_changed {
644                                            true
645                                        } else if deps.iter().any(|&dep_fwd_idx| {
646                                            curr_changed[fwd_row_base + dep_fwd_idx]
647                                        }) {
648                                            true
649                                        } else if scan_from_down {
650                                            if row_offset + 1 < iter_count {
651                                                let next_offset =
652                                                    (row_offset + 1) * forward_cols.len();
653                                                column.has_forward_ref
654                                                    && deps.iter().any(|&dep_fwd_idx| {
655                                                        curr_changed[next_offset + dep_fwd_idx]
656                                                    })
657                                            } else {
658                                                false
659                                            }
660                                        } else {
661                                            if row_offset > 0 {
662                                                let prev_offset =
663                                                    (row_offset - 1) * forward_cols.len();
664                                                !column.has_forward_ref
665                                                    && deps.iter().any(|&dep_fwd_idx| {
666                                                        curr_changed[prev_offset + dep_fwd_idx]
667                                                    })
668                                            } else {
669                                                false
670                                            }
671                                        }
672                                    };
673
674                                    if should_evaluate {
675                                        let value = match col_logics[col_idx] {
676                                            Some(compiled) => {
677                                                if is_pure_arithmetic(compiled) {
678                                                    if let Ok(Some(num)) =
679                                                        lib.engine.run_precompiled_f64_with_context(
680                                                            compiled,
681                                                            scope_data.data(),
682                                                            &ctx_value,
683                                                        )
684                                                    {
685                                                        lib.engine.f64_to_value(num)
686                                                    } else {
687                                                        lib.engine
688                                                            .run_precompiled_with_context(
689                                                                compiled,
690                                                                scope_data.data(),
691                                                                &ctx_value,
692                                                            )
693                                                            .unwrap_or(Value::Null)
694                                                    }
695                                                } else {
696                                                    lib.engine
697                                                        .run_precompiled_with_context(
698                                                            compiled,
699                                                            scope_data.data(),
700                                                            &ctx_value,
701                                                        )
702                                                        .unwrap_or(Value::Null)
703                                                }
704                                            }
705                                            None => column
706                                                .literal
707                                                .as_ref()
708                                                .map(|arc_val| Value::clone(arc_val))
709                                                .unwrap_or(Value::Null),
710                                        };
711
712                                        // Write directly to flat_cells
713                                        let cell_idx = row_base + col_idx;
714                                        if flat_cells[cell_idx] != value {
715                                            any_changed = true;
716                                            curr_changed[fwd_row_base + fwd_idx] = true;
717                                            flat_cells[cell_idx] = value;
718                                        }
719                                    }
720                                }};
721                            }
722
723                            for (fwd_idx, &col_idx) in forward_cols.iter().enumerate() {
724                                eval_col!(fwd_idx, col_idx);
725                            }
726                        }
727
728                        scan_from_down = !scan_from_down;
729                        std::mem::swap(&mut prev_changed, &mut curr_changed);
730
731                        if !any_changed {
732                            break;
733                        }
734                    }
735
736                    if let Some(start) = _backward_start {
737                        crate::utils::record_timing(
738                            &format!(
739                                "[table::{}] backward-pass rows={} sweeps={}",
740                                eval_key, iter_count, total_sweeps
741                            ),
742                            start.elapsed(),
743                        );
744                    }
745                }
746
747                // Assemble evaluated rows into local_rows in a single final pass
748                local_rows.reserve(total_rows);
749                if total_rows > 0 {
750                    let mut prototype_map = Map::with_capacity(col_count);
751                    for name in &col_names {
752                        prototype_map.insert(name.clone(), Value::Null);
753                    }
754
755                    for r in 0..total_rows {
756                        let mut row_map = prototype_map.clone();
757                        let row_offset = r * col_count;
758                        for (slot, cell) in row_map
759                            .values_mut()
760                            .zip(&mut flat_cells[row_offset..row_offset + col_count])
761                        {
762                            *slot = std::mem::replace(cell, Value::Null);
763                        }
764                        local_rows.push(Value::Object(row_map));
765                    }
766                }
767
768                // _scope_guard dropped here → TableScope cleared on evaluator
769            }
770        }
771    }
772
773    Ok((
774        std::sync::Arc::new(Value::Array(local_rows)),
775        Some(external_deps),
776    ))
777}