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}