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}