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