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