Skip to main content

json_eval_rs/jsoneval/
evaluate.rs

1use std::sync::Arc;
2
3use super::JSONEval;
4use crate::jsoneval::cancellation::CancellationToken;
5use crate::jsoneval::eval_data::EvalData;
6use crate::jsoneval::json_parser;
7use crate::jsoneval::path_utils;
8use crate::jsoneval::table_evaluate;
9use crate::time_block;
10use crate::utils::clean_float_noise_scalar;
11
12use serde_json::Value;
13
14/// Returns `true` if `new_item` (raw user input) is identity-compatible with `old_item`
15/// (snapshot that may contain computed formula outputs alongside raw input fields).
16///
17/// A full `==` comparison fails when `old_item` has extra keys written by formula evaluation
18/// (e.g., computed fields, calculated formulas) that are absent from the raw `new_item`. This helper
19/// compares only the fields present in `new_item`, ignoring extra keys in `old_item`:
20///
21/// - If both are objects: every key in `new` must match the same key in `old`.
22/// - Otherwise: standard equality (covers Null, scalar, array cases).
23///
24/// Used by `invalidate_subform_caches_on_structural_change` to detect genuine order/identity
25/// shifts without false positives from computed formula output fields in the snapshot.
26fn items_same_input_identity(old: Option<&Value>, new: Option<&Value>) -> bool {
27    match (old, new) {
28        (Some(Value::Object(old_map)), Some(Value::Object(new_map))) => new_map
29            .iter()
30            .all(|(k, new_val)| old_map.get(k).map_or(false, |old_val| old_val == new_val)),
31        (old, new) => old == new,
32    }
33}
34
35impl JSONEval {
36    /// Evaluate the schema with the given data and context.
37    ///
38    /// # Arguments
39    ///
40    /// * `data` - The data to evaluate.
41    /// * `context` - The context to evaluate.
42    ///
43    /// # Returns
44    ///
45    /// A `Result` indicating success or an error message.
46    pub fn evaluate(
47        &mut self,
48        data: &str,
49        context: Option<&str>,
50        paths: Option<&[String]>,
51        token: Option<&CancellationToken>,
52    ) -> Result<(), String> {
53        if let Some(t) = token {
54            if t.is_cancelled() {
55                return Err("Cancelled".to_string());
56            }
57        }
58        time_block!("evaluate() [total]", {
59            // Use SIMD-accelerated JSON parsing
60            // Parse and update data/context
61            let data_value = time_block!("  parse data", { json_parser::parse_json_str(data)? });
62            let context_value = time_block!("  parse context", {
63                if let Some(ctx) = context {
64                    json_parser::parse_json_str(ctx)?
65                } else {
66                    Value::Object(serde_json::Map::new())
67                }
68            });
69            self.evaluate_internal_with_new_data(data_value, context_value, paths, token)
70        })
71    }
72
73    /// Internal helper to evaluate with all data/context provided as Values.
74    /// `pub(crate)` so the cache-swap path in `evaluate_subform` can call it directly
75    /// after swapping the parent cache in, bypassing the string-parsing overhead.
76    pub(crate) fn evaluate_internal_with_new_data(
77        &mut self,
78        data: Value,
79        context: Value,
80        paths: Option<&[String]>,
81        token: Option<&CancellationToken>,
82    ) -> Result<(), String> {
83        time_block!("  evaluate_internal_with_new_data", {
84            // Reuse the previously stored snapshot as `old_data` to avoid an O(n) deep clone
85            // on every main-form evaluation call.
86            let has_previous_eval = self.eval_cache.main_form_snapshot.is_some();
87            let old_data = self
88                .eval_cache
89                .main_form_snapshot
90                .take()
91                .unwrap_or_else(|| self.eval_data.snapshot_data());
92
93            let old_context = self
94                .eval_data
95                .data()
96                .get("$context")
97                .cloned()
98                .unwrap_or(Value::Null);
99
100            // Store data, context and replace in eval_data (clone once instead of twice)
101            self.data = data.clone();
102            self.context = context.clone();
103            time_block!("  replace_data_and_context", {
104                self.eval_data.replace_data_and_context(data, context);
105            });
106
107            let new_data = self.eval_data.snapshot_data();
108            let new_context = self
109                .eval_data
110                .data()
111                .get("$context")
112                .cloned()
113                .unwrap_or(Value::Null);
114
115            if has_previous_eval
116                && old_data == new_data
117                && old_context == new_context
118                && paths.is_none()
119            {
120                // Perfect cache hit for unmodified payload: fully skip tree traversal.
121                // Restore snapshot since nothing changed.
122                self.eval_cache.main_form_snapshot = Some(new_data);
123                return Ok(());
124            }
125
126            // Seed subform caches from loaded data.
127            for (subform_path, subform) in &mut self.subforms {
128                let subform_ptr =
129                    crate::jsoneval::path_utils::schema_path_to_data_pointer(subform_path);
130                if let Some(items) = new_data.pointer(&subform_ptr).and_then(|v| v.as_array()) {
131                    for (idx, item_val) in items.iter().enumerate() {
132                        self.eval_cache.ensure_active_item_cache(idx);
133                        if let Some(c) = self.eval_cache.subform_caches.get_mut(&idx) {
134                            c.item_snapshot = item_val.clone();
135                        }
136                        subform.eval_cache.ensure_active_item_cache(idx);
137                        if let Some(c) = subform.eval_cache.subform_caches.get_mut(&idx) {
138                            c.item_snapshot = item_val.clone();
139                        }
140                    }
141                }
142            }
143
144            self.eval_cache
145                .store_snapshot_and_diff_versions(&old_data, &new_data);
146            // Save snapshot for the next evaluation cycle (O(1) Arc clone)
147            self.eval_cache.main_form_snapshot = Some(std::sync::Arc::clone(&new_data));
148
149            // Invalidate subform caches after structural changes.
150            self.invalidate_subform_caches_on_structural_change(&old_data, &new_data);
151
152            // Skip external traversal when cached dependencies are fresh.
153            if paths.is_none() && !self.eval_cache.needs_full_evaluation() {
154                self.evaluate_others(paths, token);
155                return Ok(());
156            }
157
158            // Apply visible defaults before final formula pass.
159            self.evaluate_internal(paths, token)?;
160            if self.apply_visible_static_defaults() {
161                self.evaluate_internal(paths, token)?;
162            }
163            Ok(())
164        })
165    }
166
167    /// Detect structural changes in subform arrays between `old_data` and `new_data`
168    /// and evict stale caches accordingly.
169    pub(crate) fn invalidate_subform_caches_on_structural_change(
170        &mut self,
171        old_data: &Value,
172        new_data: &Value,
173    ) {
174        for (subform_path, _) in &self.subforms {
175            // Resolve the data pointer for this subform
176            // (e.g., `/items/sub_items`)
177            let subform_ptr =
178                crate::jsoneval::path_utils::schema_path_to_data_pointer(subform_path).to_string();
179
180            let old_items = old_data.pointer(&subform_ptr).and_then(Value::as_array);
181            let new_items = new_data.pointer(&subform_ptr).and_then(Value::as_array);
182
183            let old_len = old_items.map(Vec::len).unwrap_or(0);
184            let new_len = new_items.map(Vec::len).unwrap_or(0);
185            let min_len = old_len.min(new_len);
186
187            // Detect reordered overlapping items.
188            let identities_shifted = (0..min_len).any(|i| {
189                let old_item = old_items.and_then(|a| a.get(i));
190                let new_item = new_items.and_then(|a| a.get(i));
191                !items_same_input_identity(old_item, new_item)
192            });
193
194            if old_len == new_len && !identities_shifted {
195                continue; // No structural change for this subform
196            }
197
198            // Build local subform path prefix.
199            let field_key = subform_ptr
200                .split('/')
201                .next_back()
202                .unwrap_or(subform_ptr.as_str());
203            let subform_dep_prefix = format!("/{}/", field_key);
204
205            // Evict affected T2 entries.
206            let mut evicted_paths: Vec<String> = Vec::new();
207            self.eval_cache.entries.retain(|eval_key, entry| {
208                let has_subform_dep = entry
209                    .dep_versions
210                    .keys()
211                    .any(|dep| dep.starts_with(&subform_dep_prefix));
212
213                if has_subform_dep {
214                    let normalized =
215                        crate::jsoneval::path_utils::schema_path_to_data_pointer(eval_key);
216                    evicted_paths.push(normalized.into_owned());
217                    false // remove entry
218                } else {
219                    true // keep
220                }
221            });
222
223            // Bump params_versions for every evicted T2 entry so downstream $params formulas
224            // (e.g., aggregate formulas over items) correctly miss their caches.
225            for path in &evicted_paths {
226                self.eval_cache
227                    .params_versions
228                    .bump(path, "invalidate_subform_caches_on_structural_change");
229            }
230
231            // Clear T1 per-item caches for indices where item identity has shifted.
232            // This prevents stale per-item results being reused for a different item
233            // occupying the same array slot after a reorder.
234            for idx in 0..min_len {
235                let old_item = old_items.and_then(|a| a.get(idx));
236                let new_item = new_items.and_then(|a| a.get(idx));
237                if !items_same_input_identity(old_item, new_item) {
238                    if let Some(c) = self.eval_cache.subform_caches.get_mut(&idx) {
239                        c.entries.clear();
240                        c.data_versions = crate::jsoneval::eval_cache::VersionTracker::new();
241                    }
242                }
243            }
244            // Prune T1 caches for indices that no longer exist (removed items)
245            self.eval_cache.prune_subform_caches(new_len);
246
247            if !evicted_paths.is_empty() || old_len != new_len {
248                self.eval_cache.eval_generation += 1;
249            }
250        }
251    }
252
253    /// Fast variant of `evaluate_internal_with_new_data` for the cache-swap path.
254    ///
255    /// The caller (e.g. `run_subform_pass` / `evaluate_subform_item`) has **already**:
256    /// 1. Called `replace_data_and_context` on `subform.eval_data` with the merged payload.
257    /// 2. Computed the item-level diff and bumped `subform_caches[idx].data_versions` accordingly.
258    /// 3. Swapped the parent cache into `subform.eval_cache` so Tier 2 entries are visible.
259    /// 4. Set `active_item_index = Some(idx)` on the swapped-in cache.
260    ///
261    /// Skipping the expensive `snapshot_data_clone()` × 2 and `diff_and_update_versions`
262    /// saves significant overhead per subform item on large payloads.
263    pub(crate) fn evaluate_internal_pre_diffed(
264        &mut self,
265        paths: Option<&[String]>,
266        token: Option<&CancellationToken>,
267    ) -> Result<(), String> {
268        debug_assert!(
269            self.eval_cache.active_item_index.is_some(),
270            "evaluate_internal_pre_diffed called without active_item_index — \
271             caller must set up the cache-swap before calling this method"
272        );
273
274        // Always delegate to evaluate_internal so that evaluated_schema is populated correctly
275        // for every item. The previous generation-based skip here left evaluated_schema stale
276        // (with the prior item's values) when no deps changed — causing get_evaluated_schema_subform
277        // to return wrong values for all but the last-evaluated item.
278        //
279        // evaluate_internal's all-hit fast path (lines ~314–338) handles the no-change case
280        // efficiently: it writes eval_data + evaluated_schema per formula from T1 cache and
281        // skips the expensive formula engine entirely.
282        self.evaluate_internal(paths, token)
283    }
284
285    /// Internal evaluate that can be called when data is already set
286    /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
287    pub(crate) fn evaluate_internal(
288        &mut self,
289        paths: Option<&[String]>,
290        token: Option<&CancellationToken>,
291    ) -> Result<(), String> {
292        if let Some(t) = token {
293            if t.is_cancelled() {
294                return Err("Cancelled".to_string());
295            }
296        }
297        time_block!("  evaluate_internal() [total]", {
298            if self.eval_cache.subform_roots.is_empty() && !self.subforms.is_empty() {
299                self.eval_cache.subform_roots = self
300                    .subforms
301                    .keys()
302                    .map(|p| format!("/{}", crate::jsoneval::dependents::subform_field_key(p)))
303                    .collect();
304            }
305
306            // Acquire lock for synchronous execution
307            let _lock = self.eval_lock.lock().unwrap();
308            let _static_guard = self
309                .engine
310                .bind_static_arrays_scope(Arc::clone(&self.static_arrays));
311
312            // Normalize paths to schema pointers for correct filtering
313            let normalized_paths_storage; // Keep alive
314            let normalized_paths = if let Some(p_list) = paths {
315                normalized_paths_storage = p_list
316                    .iter()
317                    .flat_map(|p| {
318                        let normalized = if p.starts_with("#/") {
319                            p.to_string()
320                        } else if p.starts_with('/') {
321                            format!("#{}", p)
322                        } else {
323                            format!("#/{}", p.replace('.', "/"))
324                        };
325                        vec![normalized]
326                    })
327                    .collect::<Vec<_>>();
328                Some(normalized_paths_storage.as_slice())
329            } else {
330                None
331            };
332
333            // Borrow sorted_evaluations via Arc (avoid deep-cloning Vec<Vec<String>>)
334            let eval_batches = self.sorted_evaluations.clone();
335
336            // Process each batch - sequentially
337            // Batches are processed sequentially to maintain dependency order
338            // Process value evaluations (simple computed fields with no dependencies)
339            time_block!("      evaluate values", {
340                for eval_key in self.value_evaluations.iter() {
341                    if let Some(t) = token {
342                        if t.is_cancelled() {
343                            return Err("Cancelled".to_string());
344                        }
345                    }
346                    // Skip if has dependencies (handled in sorted batches with correct ordering)
347                    if let Some(deps) = self.dependencies.get(eval_key) {
348                        if !deps.is_empty() {
349                            continue;
350                        }
351                    }
352
353                    // Filter items if paths are provided
354                    if let Some(filter_paths) = normalized_paths {
355                        if !filter_paths.is_empty()
356                            && !filter_paths.iter().any(|p| {
357                                eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())
358                            })
359                        {
360                            continue;
361                        }
362                    }
363
364                    let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
365                    let empty_deps = indexmap::IndexSet::new();
366                    let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
367
368                    // Cache hit check
369                    if let Some(_cached_result) = self.eval_cache.check_cache(eval_key, deps) {
370                        continue;
371                    }
372
373                    // Cache miss - evaluate
374                    if let Some(logic_id) = self.evaluations.get(eval_key) {
375                        let val = {
376                            let snap = self.eval_data.snapshot_data();
377                            self.engine.run(logic_id, &*snap)
378                        };
379                        match val {
380                            Ok(val) => {
381                                let cleaned_val = clean_float_noise_scalar(val);
382                                self.eval_cache
383                                    .store_cache(eval_key, deps, cleaned_val.clone());
384
385                                if let Some(pointer_value) =
386                                    self.evaluated_schema.pointer_mut(&pointer_path)
387                                {
388                                    *pointer_value = cleaned_val;
389                                }
390                            }
391                            Err(_) => {
392                                // Formula failed — ensure no raw $evaluation object leaks.
393                                // Write null only if the node still holds the unevaluated formula.
394                                if let Some(node) = self.evaluated_schema.pointer_mut(&pointer_path)
395                                {
396                                    if node.is_object() && node.get("$evaluation").is_some() {
397                                        *node = Value::Null;
398                                    }
399                                }
400                            }
401                        }
402                    }
403                }
404            });
405
406            time_block!("    process batches", {
407                for batch in eval_batches.iter() {
408                    if let Some(t) = token {
409                        if t.is_cancelled() {
410                            return Err("Cancelled".to_string());
411                        }
412                    }
413                    // Skip empty batches
414                    if batch.is_empty() {
415                        continue;
416                    }
417
418                    // Check if we can skip this entire batch optimization
419                    let batch_skipped = time_block!("      batch filter check", {
420                        if let Some(filter_paths) = normalized_paths {
421                            if !filter_paths.is_empty() {
422                                let batch_has_match = batch.iter().any(|eval_key| {
423                                    filter_paths.iter().any(|p| {
424                                        eval_key.starts_with(p.as_str())
425                                            || (p.starts_with(eval_key.as_str())
426                                                && !eval_key.contains("/$params/"))
427                                    })
428                                });
429                                !batch_has_match
430                            } else {
431                                false
432                            }
433                        } else {
434                            false
435                        }
436                    });
437                    if batch_skipped {
438                        continue;
439                    }
440
441                    // Fast path: try to resolve every eval_key in this batch from cache.
442                    // If all hit, skip the expensive exclusive_clone() of the full eval_data tree.
443                    // This is critical for subforms where eval_data contains the full parent payload.
444                    let mut batch_hits: Vec<(String, String, std::sync::Arc<Value>)> =
445                        Vec::with_capacity(batch.len());
446                    let all_hit = batch.iter().all(|eval_key| {
447                        let empty_deps = indexmap::IndexSet::new();
448                        let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
449                        let cached = if self.table_metadata.contains_key(eval_key) {
450                            self.eval_cache.check_table_cache(eval_key, deps)
451                        } else {
452                            self.eval_cache.check_cache_arc(eval_key, deps)
453                        };
454                        if let Some(cached_arc) = cached {
455                            let pointer_path =
456                                path_utils::normalize_to_json_pointer(eval_key).into_owned();
457                            batch_hits.push((eval_key.clone(), pointer_path, cached_arc));
458                            true
459                        } else {
460                            false
461                        }
462                    });
463
464                    if all_hit {
465                        // Populate eval_data AND evaluated_schema so both downstream batches
466                        // and get_evaluated_schema callers see the correct per-item values.
467                        for (eval_key, ptr, arc_val) in batch_hits {
468                            if self.table_metadata.contains_key(&eval_key) {
469                                let static_key = format!("/$table{}", ptr);
470                                Arc::make_mut(&mut self.static_arrays)
471                                    .insert(static_key.clone(), std::sync::Arc::clone(&arc_val));
472                                let marker = serde_json::json!({ "$static_array": static_key });
473                                self.eval_data.set(&ptr, marker.clone());
474                                self.engine
475                                    .set_static_arrays(std::sync::Arc::clone(&self.static_arrays));
476                                if let Some(schema_value) = self.evaluated_schema.pointer_mut(&ptr)
477                                {
478                                    *schema_value = marker;
479                                }
480                            } else {
481                                self.eval_data.set(&ptr, (*arc_val).clone());
482                                if let Some(schema_value) = self.evaluated_schema.pointer_mut(&ptr)
483                                {
484                                    *schema_value = (*arc_val).clone();
485                                }
486                            }
487                        }
488                        continue;
489                    }
490
491                    // Sequential execution.
492                    // For each formula miss, snapshot_data() gives an O(1) Arc::clone
493                    // as a stable read view. The Arc is dropped before self.eval_data.set()
494                    // so Arc::make_mut always finds rc=1 — zero deep copy, zero latency.
495                    time_block!("      batch sequential eval", {
496                        for eval_key in batch {
497                            if let Some(t) = token {
498                                if t.is_cancelled() {
499                                    return Err("Cancelled".to_string());
500                                }
501                            }
502                            // Filter individual items if paths are provided
503                            if let Some(filter_paths) = normalized_paths {
504                                if !filter_paths.is_empty()
505                                    && !filter_paths.iter().any(|p| {
506                                        eval_key.starts_with(p.as_str())
507                                            || (p.starts_with(eval_key.as_str())
508                                                && !eval_key.contains("/$params/"))
509                                    })
510                                {
511                                    continue;
512                                }
513                            }
514
515                            let pointer_path =
516                                path_utils::normalize_to_json_pointer(eval_key).into_owned();
517
518                            // Cache miss - evaluate
519                            let is_table = self.table_metadata.contains_key(eval_key);
520
521                            if is_table {
522                                let empty_deps = indexmap::IndexSet::new();
523                                let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
524                                if let Some(cached_arc) =
525                                    self.eval_cache.check_table_cache(eval_key, deps)
526                                {
527                                    let static_key = format!("/$table{}", pointer_path);
528                                    Arc::make_mut(&mut self.static_arrays).insert(
529                                        static_key.clone(),
530                                        std::sync::Arc::clone(&cached_arc),
531                                    );
532                                    let marker = serde_json::json!({ "$static_array": static_key });
533                                    self.eval_data.set(&pointer_path, marker.clone());
534                                    self.engine.set_static_arrays(std::sync::Arc::clone(
535                                        &self.static_arrays,
536                                    ));
537                                    if let Some(schema_value) =
538                                        self.evaluated_schema.pointer_mut(&pointer_path)
539                                    {
540                                        *schema_value = marker;
541                                    }
542                                    continue;
543                                }
544                                time_block!("        table eval", {
545                                    // Snapshot for table read access: Arc::clone is O(1).
546                                    // Scoped so it's dropped before self.eval_data.set() below,
547                                    // keeping self.eval_data.data at rc=1 so Arc::make_mut is free.
548                                    let table_result = {
549                                        let table_scope =
550                                            EvalData::from_arc(self.eval_data.snapshot_data());
551                                        table_evaluate::evaluate_table(
552                                            self,
553                                            eval_key,
554                                            &table_scope,
555                                            token,
556                                        )
557                                        // table_scope dropped here → rc back to 1
558                                    };
559                                    if let Ok((arc_value, external_deps_opt)) = table_result {
560                                        if let Some(external_deps) = external_deps_opt {
561                                            self.eval_cache.store_cache_arc(
562                                                eval_key,
563                                                &external_deps,
564                                                std::sync::Arc::clone(&arc_value),
565                                            );
566                                        }
567
568                                        // NOTE: bump_params_version / bump_data_version for table results
569                                        // is now handled inside store_cache (conditional on value change).
570                                        // The separate bump here was double-counting: store_cache uses T2
571                                        // comparison while this block used eval_data as reference point,
572                                        // causing two version increments per changed table.
573
574                                        let static_key = format!("/$table{}", pointer_path);
575
576                                        Arc::make_mut(&mut self.static_arrays).insert(
577                                            static_key.clone(),
578                                            std::sync::Arc::clone(&arc_value),
579                                        );
580
581                                        let marker =
582                                            serde_json::json!({ "$static_array": static_key });
583                                        self.eval_data.set(&pointer_path, marker.clone());
584                                        self.engine
585                                            .set_static_arrays(Arc::clone(&self.static_arrays));
586
587                                        if let Some(schema_value) =
588                                            self.evaluated_schema.pointer_mut(&pointer_path)
589                                        {
590                                            *schema_value = marker;
591                                        }
592                                    }
593                                });
594                            } else {
595                                let empty_deps = indexmap::IndexSet::new();
596                                let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
597                                let cached_result = self.eval_cache.check_cache(eval_key, &deps);
598
599                                time_block!("        formula eval", {
600                                    if let Some(cached_result) = cached_result {
601                                        // Must still populate eval_data out of cache so subsequent formulas
602                                        // referencing this path in the same iteration can read the exact value
603                                        self.eval_data.set(&pointer_path, cached_result.clone());
604                                        if let Some(schema_value) =
605                                            self.evaluated_schema.pointer_mut(&pointer_path)
606                                        {
607                                            *schema_value = cached_result;
608                                        }
609                                    } else if let Some(logic_id) = self.evaluations.get(eval_key) {
610                                        // snapshot_data() is O(1) Arc::clone — no deep copy.
611                                        // Arc is moved into `snap` and lives only for the
612                                        // engine.run() call, then dropped before set() below.
613                                        // This keeps self.eval_data.data at rc=1 when set()
614                                        // calls Arc::make_mut, so no deep clone ever occurs.
615                                        let val = {
616                                            let snap = self.eval_data.snapshot_data();
617                                            self.engine.run(logic_id, &*snap)
618                                            // snap dropped here → rc back to 1
619                                        };
620                                        match val {
621                                            Ok(val) => {
622                                                let cleaned_val = clean_float_noise_scalar(val);
623                                                let data_path = crate::jsoneval::path_utils::schema_path_to_data_pointer(&pointer_path).into_owned();
624                                                self.eval_cache.store_cache(
625                                                    eval_key,
626                                                    &deps,
627                                                    cleaned_val.clone(),
628                                                );
629
630                                                // Bump data_versions when non-$params field value changes.
631                                                // $params bumps are handled inside store_cache (conditional).
632                                                let old_val = self
633                                                    .eval_data
634                                                    .get(&data_path)
635                                                    .cloned()
636                                                    .unwrap_or(Value::Null);
637                                                if cleaned_val != old_val
638                                                    && !data_path.starts_with("/$params")
639                                                {
640                                                    self.eval_cache.bump_data_version(&data_path);
641                                                }
642
643                                                self.eval_data
644                                                    .set(&pointer_path, cleaned_val.clone());
645                                                if let Some(schema_value) =
646                                                    self.evaluated_schema.pointer_mut(&pointer_path)
647                                                {
648                                                    *schema_value = cleaned_val;
649                                                }
650                                            }
651                                            Err(_) => {
652                                                // Formula failed — ensure no raw $evaluation object leaks.
653                                                // Write null only if the node still holds the unevaluated formula.
654                                                if let Some(node) =
655                                                    self.evaluated_schema.pointer_mut(&pointer_path)
656                                                {
657                                                    if node.is_object()
658                                                        && node.get("$evaluation").is_some()
659                                                    {
660                                                        *node = Value::Null;
661                                                    }
662                                                }
663                                            }
664                                        }
665                                    }
666                                });
667                            }
668                        }
669                    });
670                }
671            });
672
673            // Drop lock before calling evaluate_others
674            drop(_lock);
675
676            // Mark generation stable so the next evaluate_internal call can detect whether
677            // any formula was actually re-stored (via bump_data/params_version) since this run.
678            self.eval_cache.mark_evaluated();
679
680            self.evaluate_others(paths, token);
681
682            Ok(())
683        })
684    }
685
686    pub(crate) fn evaluate_others(
687        &mut self,
688        paths: Option<&[String]>,
689        token: Option<&CancellationToken>,
690    ) {
691        if let Some(t) = token {
692            if t.is_cancelled() {
693                return;
694            }
695        }
696        time_block!("    evaluate_others()", {
697            // Step 1: Evaluate "rules" and "others" categories with caching
698            // Rules are evaluated here so their values are available in evaluated_schema
699            let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
700            if combined_count > 0 {
701                time_block!("      evaluate rules+others", {
702                    let eval_data_snapshot = self.eval_data.clone();
703
704                    let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
705                        p_list
706                            .iter()
707                            .flat_map(|p| {
708                                let ptr = path_utils::dot_notation_to_schema_pointer(p);
709                                // Also support version with /properties/ prefix for root match
710                                let with_props = if ptr.starts_with("#/") {
711                                    format!("#/properties/{}", &ptr[2..])
712                                } else {
713                                    ptr.clone()
714                                };
715                                vec![ptr, with_props]
716                            })
717                            .collect()
718                    });
719
720                    // Sequential evaluation
721                    let combined_evals: Vec<&String> = self
722                        .rules_evaluations
723                        .iter()
724                        .chain(self.others_evaluations.iter())
725                        .collect();
726
727                    for eval_key in combined_evals {
728                        if let Some(t) = token {
729                            if t.is_cancelled() {
730                                return;
731                            }
732                        }
733
734                        // // Defer options array evaluation — only the root /options field,
735                        // // not its children (e.g. /options/0/label are still evaluated normally).
736                        // // Call get_field_options() to resolve on demand.
737                        // if eval_key.ends_with("/options") {
738                        //     continue;
739                        // }
740
741                        // Filter items if paths are provided
742                        if let Some(filter_paths) = normalized_paths.as_ref() {
743                            if !filter_paths.is_empty()
744                                && !filter_paths.iter().any(|p| {
745                                    eval_key.starts_with(p.as_str())
746                                        || (p.starts_with(eval_key.as_str())
747                                            && !eval_key.contains("/$params/"))
748                                })
749                            {
750                                continue;
751                            }
752                        }
753
754                        let pointer_path =
755                            path_utils::normalize_to_json_pointer(eval_key).into_owned();
756                        let empty_deps = indexmap::IndexSet::new();
757                        let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
758
759                        if let Some(cached_result) = self.eval_cache.check_cache(eval_key, &deps) {
760                            if let Some(pointer_value) =
761                                self.evaluated_schema.pointer_mut(&pointer_path)
762                            {
763                                if !pointer_path.starts_with("$")
764                                    && pointer_path.contains("/rules/")
765                                    && !pointer_path.ends_with("/value")
766                                {
767                                    if let Some(pointer_obj) = pointer_value.as_object_mut() {
768                                        pointer_obj.remove("$evaluation");
769                                        pointer_obj
770                                            .insert("value".to_string(), cached_result.clone());
771                                    }
772                                } else {
773                                    *pointer_value = cached_result.clone();
774                                }
775                            }
776                            continue;
777                        }
778                        if let Some(logic_id) = self.evaluations.get(eval_key) {
779                            match self.engine.run(logic_id, eval_data_snapshot.data()) {
780                                Ok(val) => {
781                                    let cleaned_val = clean_float_noise_scalar(val);
782                                    self.eval_cache.store_cache(
783                                        eval_key,
784                                        &deps,
785                                        cleaned_val.clone(),
786                                    );
787
788                                    if let Some(pointer_value) =
789                                        self.evaluated_schema.pointer_mut(&pointer_path)
790                                    {
791                                        if !pointer_path.starts_with("$")
792                                            && pointer_path.contains("/rules/")
793                                            && !pointer_path.ends_with("/value")
794                                        {
795                                            match pointer_value.as_object_mut() {
796                                                Some(pointer_obj) => {
797                                                    pointer_obj.remove("$evaluation");
798                                                    pointer_obj
799                                                        .insert("value".to_string(), cleaned_val);
800                                                }
801                                                None => continue,
802                                            }
803                                        } else {
804                                            *pointer_value = cleaned_val;
805                                        }
806                                    }
807                                }
808                                Err(_) => {
809                                    // Formula failed — ensure no raw $evaluation object leaks.
810                                    // Write null only if the node still holds the unevaluated formula.
811                                    if let Some(node) =
812                                        self.evaluated_schema.pointer_mut(&pointer_path)
813                                    {
814                                        if node.is_object() && node.get("$evaluation").is_some() {
815                                            *node = Value::Null;
816                                        }
817                                    }
818                                }
819                            }
820                        }
821                    }
822                });
823            }
824        });
825
826        self.refresh_computed_value_dependents(token);
827        self.evaluate_options_templates(paths);
828
829        self.invalidate_layout_cache();
830    }
831
832    /// Re-evaluate direct dependents of computed fields against a temporary data overlay.
833    /// Computed values are exposed only for this refresh; shared form data, cache entries and
834    /// version trackers remain untouched, preventing subform/table cascade contamination.
835    fn refresh_computed_value_dependents(&mut self, token: Option<&CancellationToken>) {
836        let mut changed = Vec::new();
837        let mut dep_matchers = indexmap::IndexSet::new();
838
839        for key in self.evaluations.keys() {
840            let Some(field_path) = key.strip_suffix("/value") else {
841                continue;
842            };
843            if !field_path.contains("/properties/") || key.contains("/rules/") {
844                continue;
845            }
846            let schema_pointer = path_utils::normalize_to_json_pointer(key);
847            let Some(value) = self.evaluated_schema.pointer(&schema_pointer) else {
848                continue;
849            };
850            if value.is_object() && value.get("$evaluation").is_some() {
851                continue;
852            }
853            let data_path = path_utils::schema_path_to_data_pointer(field_path).into_owned();
854            if self.eval_data.get(&data_path) != Some(value) {
855                dep_matchers.insert(data_path.clone());
856                dep_matchers.insert(field_path.to_string());
857                dep_matchers.insert(field_path.trim_start_matches('#').to_string());
858                changed.push((data_path, value.clone()));
859            }
860        }
861
862        if changed.is_empty() {
863            return;
864        }
865
866        let targets: Vec<String> = self
867            .evaluations
868            .keys()
869            .filter(|key| {
870                !key.contains("/dependents/")
871                    && !key.contains("/$params/")
872                    && !self.tables.keys().any(|table| key.starts_with(table))
873                    && self.dependencies.get(*key).is_some_and(|dependencies| {
874                        dependencies.iter().any(|dependency| {
875                            dep_matchers.contains(dependency.as_str())
876                                || dep_matchers.contains(
877                                    path_utils::schema_path_to_data_pointer(dependency).as_ref(),
878                                )
879                        })
880                    })
881            })
882            .cloned()
883            .collect();
884
885        if targets.is_empty() {
886            return;
887        }
888
889        let mut overlay = EvalData::new(self.eval_data.snapshot_data_clone());
890        for (data_path, value) in changed {
891            overlay.set(&data_path, value);
892        }
893
894        for key in targets {
895            if token.is_some_and(CancellationToken::is_cancelled) {
896                return;
897            }
898            let Some(logic_id) = self.evaluations.get(&key) else {
899                continue;
900            };
901            let Ok(value) = self.engine.run(logic_id, overlay.data()) else {
902                continue;
903            };
904            let pointer = path_utils::normalize_to_json_pointer(&key);
905            if let Some(node) = self.evaluated_schema.pointer_mut(&pointer) {
906                let value = clean_float_noise_scalar(value);
907                if pointer.contains("/rules/") && !pointer.ends_with("/value") {
908                    if let Some(rule) = node.as_object_mut() {
909                        rule.remove("$evaluation");
910                        rule.insert("value".to_string(), value);
911                    }
912                } else {
913                    *node = value;
914                }
915            }
916        }
917    }
918
919    /// Evaluate options URL templates (handles {variable} patterns) — called on demand from get_field_options
920    #[allow(dead_code)]
921    pub(crate) fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
922        // Use pre-collected options templates from parsing (Arc clone is cheap)
923        let templates_to_eval = self.options_templates.clone();
924
925        // Evaluate each template
926        for (path, template_str, params_path) in templates_to_eval.iter() {
927            // Filter items if paths are provided
928            // 'path' here is the schema path to the field (dot notation or similar, need to check)
929            // It seems to be schema pointer based on usage in other methods
930            if let Some(filter_paths) = paths {
931                if !filter_paths.is_empty()
932                    && !filter_paths
933                        .iter()
934                        .any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str()))
935                {
936                    continue;
937                }
938            }
939
940            if let Some(params) = self.evaluated_schema.pointer(&params_path) {
941                if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
942                    if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
943                        *target = Value::String(evaluated);
944                    }
945                }
946            }
947        }
948    }
949
950    /// Evaluate a template string like "api/users/{id}" with params
951    pub(crate) fn evaluate_template(
952        &self,
953        template: &str,
954        params: &Value,
955    ) -> Result<String, String> {
956        let mut result = template.to_string();
957
958        // Simple template evaluation: replace {key} with params.key
959        if let Value::Object(params_map) = params {
960            for (key, value) in params_map {
961                let placeholder = format!("{{{}}}", key);
962                if let Some(str_val) = value.as_str() {
963                    result = result.replace(&placeholder, str_val);
964                } else {
965                    // Convert non-string values to strings
966                    result = result.replace(&placeholder, &value.to_string());
967                }
968            }
969        }
970
971        Ok(result)
972    }
973}