json_eval_rs/jsoneval/evaluate.rs
1use std::sync::Arc;
2
3use super::JSONEval;
4use crate::jsoneval::cancellation::CancellationToken;
5use crate::jsoneval::json_parser;
6use crate::jsoneval::path_utils;
7use crate::jsoneval::table_evaluate;
8use crate::time_block;
9use crate::utils::clean_float_noise_scalar;
10
11use serde_json::Value;
12
13impl JSONEval {
14 /// Evaluate the schema with the given data and context.
15 ///
16 /// # Arguments
17 ///
18 /// * `data` - The data to evaluate.
19 /// * `context` - The context to evaluate.
20 ///
21 /// # Returns
22 ///
23 /// A `Result` indicating success or an error message.
24 pub fn evaluate(
25 &mut self,
26 data: &str,
27 context: Option<&str>,
28 paths: Option<&[String]>,
29 token: Option<&CancellationToken>,
30 ) -> Result<(), String> {
31 if let Some(t) = token {
32 if t.is_cancelled() {
33 return Err("Cancelled".to_string());
34 }
35 }
36 time_block!("evaluate() [total]", {
37 // Use SIMD-accelerated JSON parsing
38 // Parse and update data/context
39 let data_value = time_block!(" parse data", { json_parser::parse_json_str(data)? });
40 let context_value = time_block!(" parse context", {
41 if let Some(ctx) = context {
42 json_parser::parse_json_str(ctx)?
43 } else {
44 Value::Object(serde_json::Map::new())
45 }
46 });
47 self.evaluate_internal_with_new_data(data_value, context_value, paths, token)
48 })
49 }
50
51 /// Internal helper to evaluate with all data/context provided as Values.
52 /// `pub(crate)` so the cache-swap path in `evaluate_subform` can call it directly
53 /// after swapping the parent cache in, bypassing the string-parsing overhead.
54 pub(crate) fn evaluate_internal_with_new_data(
55 &mut self,
56 data: Value,
57 context: Value,
58 paths: Option<&[String]>,
59 token: Option<&CancellationToken>,
60 ) -> Result<(), String> {
61 time_block!(" evaluate_internal_with_new_data", {
62 // Reuse the previously stored snapshot as `old_data` to avoid an O(n) deep clone
63 // on every main-form evaluation call.
64 let has_previous_eval = self.eval_cache.main_form_snapshot.is_some();
65 let old_data = self
66 .eval_cache
67 .main_form_snapshot
68 .take()
69 .unwrap_or_else(|| self.eval_data.snapshot_data_clone());
70
71 let old_context = self
72 .eval_data
73 .data()
74 .get("$context")
75 .cloned()
76 .unwrap_or(Value::Null);
77
78 // Store data, context and replace in eval_data (clone once instead of twice)
79 self.data = data.clone();
80 self.context = context.clone();
81 time_block!(" replace_data_and_context", {
82 self.eval_data.replace_data_and_context(data, context);
83 });
84
85 let new_data = self.eval_data.snapshot_data_clone();
86 let new_context = self
87 .eval_data
88 .data()
89 .get("$context")
90 .cloned()
91 .unwrap_or(Value::Null);
92
93 if has_previous_eval
94 && old_data == new_data
95 && old_context == new_context
96 && paths.is_none()
97 {
98 // Perfect cache hit for unmodified payload: fully skip tree traversal.
99 // Restore snapshot since nothing changed.
100 self.eval_cache.main_form_snapshot = Some(new_data);
101 return Ok(());
102 }
103
104 self.eval_cache
105 .store_snapshot_and_diff_versions(&old_data, &new_data);
106 // Save snapshot for the next evaluation cycle (avoids one snapshot_data_clone() call).
107 self.eval_cache.main_form_snapshot = Some(new_data);
108
109 // Generation-based fast skip: diff_and_update_versions bumps data_versions.versions
110 // but does NOT increment eval_generation. Only bump_data_version / bump_params_version
111 // (called from formula stores) advance eval_generation.
112 // If eval_generation == last_evaluated_generation after the diff, no formula's cached
113 // deps are actually stale — all batches would be cache hits. Skip the full traversal.
114 // Safe only in the external evaluate() path; run_re_evaluate_pass must always evaluate.
115 if paths.is_none() && !self.eval_cache.needs_full_evaluation() {
116 self.evaluate_others(paths, token, false);
117 return Ok(());
118 }
119
120 // Call internal evaluate (uses existing data if not provided)
121 self.evaluate_internal(paths, token)
122 })
123 }
124
125 /// Fast variant of `evaluate_internal_with_new_data` for the cache-swap path.
126 ///
127 /// The caller (e.g. `run_subform_pass` / `evaluate_subform_item`) has **already**:
128 /// 1. Called `replace_data_and_context` on `subform.eval_data` with the merged payload.
129 /// 2. Computed the item-level diff and bumped `subform_caches[idx].data_versions` accordingly.
130 /// 3. Swapped the parent cache into `subform.eval_cache` so Tier 2 entries are visible.
131 /// 4. Set `active_item_index = Some(idx)` on the swapped-in cache.
132 ///
133 /// Skipping the expensive `snapshot_data_clone()` × 2 and `diff_and_update_versions`
134 /// saves ~40–80ms per rider on a 5 MB parent payload.
135 pub(crate) fn evaluate_internal_pre_diffed(
136 &mut self,
137 paths: Option<&[String]>,
138 token: Option<&CancellationToken>,
139 ) -> Result<(), String> {
140 debug_assert!(
141 self.eval_cache.active_item_index.is_some(),
142 "evaluate_internal_pre_diffed called without active_item_index — \
143 caller must set up the cache-swap before calling this method"
144 );
145
146 // Same generation-based fast skip as evaluate_internal_with_new_data:
147 // The diff_and_update_versions calls in with_item_cache_swap bump data_versions.versions
148 // but do NOT increment eval_generation. If nothing was re-stored since last evaluate, skip.
149 if paths.is_none() && !self.eval_cache.needs_full_evaluation() {
150 self.evaluate_others(paths, token, false);
151 return Ok(());
152 }
153
154 self.evaluate_internal(paths, token)
155 }
156
157 /// Internal evaluate that can be called when data is already set
158 /// This avoids double-locking and unnecessary data cloning for re-evaluation from evaluate_dependents
159 pub(crate) fn evaluate_internal(
160 &mut self,
161 paths: Option<&[String]>,
162 token: Option<&CancellationToken>,
163 ) -> Result<(), String> {
164 if let Some(t) = token {
165 if t.is_cancelled() {
166 return Err("Cancelled".to_string());
167 }
168 }
169 time_block!(" evaluate_internal() [total]", {
170 // Acquire lock for synchronous execution
171 let _lock = self.eval_lock.lock().unwrap();
172
173 // Normalize paths to schema pointers for correct filtering
174 let normalized_paths_storage; // Keep alive
175 let normalized_paths = if let Some(p_list) = paths {
176 normalized_paths_storage = p_list
177 .iter()
178 .flat_map(|p| {
179 let normalized = if p.starts_with("#/") {
180 p.to_string()
181 } else if p.starts_with('/') {
182 format!("#{}", p)
183 } else {
184 format!("#/{}", p.replace('.', "/"))
185 };
186 vec![normalized]
187 })
188 .collect::<Vec<_>>();
189 Some(normalized_paths_storage.as_slice())
190 } else {
191 None
192 };
193
194 // Borrow sorted_evaluations via Arc (avoid deep-cloning Vec<Vec<String>>)
195 let eval_batches = self.sorted_evaluations.clone();
196
197 // Track whether any entry was a cache miss (required an actual formula run).
198 // When false (all hits), evaluate_others can skip resolve_layout because no
199 // values changed and the layout state is guaranteed identical.
200 // On the very first evaluation (last_evaluated_generation == u64::MAX), we MUST
201 // force a cache miss so that static schemas (with no formulas) still process
202 // URL templates and layout resolution once.
203 let mut had_cache_miss = self.eval_cache.last_evaluated_generation == u64::MAX;
204
205 // Process each batch - sequentially
206 // Batches are processed sequentially to maintain dependency order
207 // Process value evaluations (simple computed fields with no dependencies)
208 let eval_data_values = self.eval_data.clone();
209 time_block!(" evaluate values", {
210 for eval_key in self.value_evaluations.iter() {
211 if let Some(t) = token {
212 if t.is_cancelled() {
213 return Err("Cancelled".to_string());
214 }
215 }
216 // Skip if has dependencies (handled in sorted batches with correct ordering)
217 if let Some(deps) = self.dependencies.get(eval_key) {
218 if !deps.is_empty() {
219 continue;
220 }
221 }
222
223 // Filter items if paths are provided
224 if let Some(filter_paths) = normalized_paths {
225 if !filter_paths.is_empty()
226 && !filter_paths.iter().any(|p| {
227 eval_key.starts_with(p.as_str()) || p.starts_with(eval_key.as_str())
228 })
229 {
230 continue;
231 }
232 }
233
234 let pointer_path = path_utils::normalize_to_json_pointer(eval_key).into_owned();
235 let empty_deps = indexmap::IndexSet::new();
236 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
237
238 // Cache hit check
239 if let Some(_cached_result) = self.eval_cache.check_cache(eval_key, deps) {
240 continue;
241 }
242
243 had_cache_miss = true;
244 // Cache miss - evaluate
245 if let Some(logic_id) = self.evaluations.get(eval_key) {
246 if let Ok(val) = self.engine.run(logic_id, eval_data_values.data()) {
247 let cleaned_val = clean_float_noise_scalar(val);
248 self.eval_cache
249 .store_cache(eval_key, deps, cleaned_val.clone());
250
251 if let Some(pointer_value) =
252 self.evaluated_schema.pointer_mut(&pointer_path)
253 {
254 *pointer_value = cleaned_val;
255 }
256 }
257 }
258 }
259 });
260
261 time_block!(" process batches", {
262 for batch in eval_batches.iter() {
263 if let Some(t) = token {
264 if t.is_cancelled() {
265 return Err("Cancelled".to_string());
266 }
267 }
268 // Skip empty batches
269 if batch.is_empty() {
270 continue;
271 }
272
273 // Check if we can skip this entire batch optimization
274 if let Some(filter_paths) = normalized_paths {
275 if !filter_paths.is_empty() {
276 let batch_has_match = batch.iter().any(|eval_key| {
277 filter_paths.iter().any(|p| {
278 eval_key.starts_with(p.as_str())
279 || (p.starts_with(eval_key.as_str())
280 && !eval_key.contains("/$params/"))
281 })
282 });
283 if !batch_has_match {
284 continue;
285 }
286 }
287 }
288
289 // Fast path: try to resolve every eval_key in this batch from cache.
290 // If all hit, skip the expensive exclusive_clone() of the full eval_data tree.
291 // This is critical for subforms where eval_data contains the full parent payload.
292 {
293 let mut batch_hits: Vec<(String, Value)> = Vec::with_capacity(batch.len());
294 let all_hit = batch.iter().all(|eval_key| {
295 let empty_deps = indexmap::IndexSet::new();
296 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
297 if let Some(cached) = self.eval_cache.check_cache(eval_key, deps) {
298 let pointer_path =
299 path_utils::normalize_to_json_pointer(eval_key).into_owned();
300 batch_hits.push((pointer_path, cached));
301 true
302 } else {
303 false
304 }
305 });
306
307 if all_hit {
308 // Populate eval_data so downstream batches see these values
309 for (ptr, val) in batch_hits {
310 self.eval_data.set(&ptr, val);
311 }
312 continue;
313 }
314 had_cache_miss = true;
315 // Partial or full miss — fall through to the normal exclusive_clone path below.
316 // batch_hits is dropped here; cache lookups will repeat but that's cheap.
317 }
318
319 // Sequential execution
320 // Use exclusive_clone() so self.eval_data.set() within this batch
321 // is always zero-cost (Arc rc stays 1 on self.eval_data).
322 let eval_data_snapshot = self.eval_data.exclusive_clone();
323
324 for eval_key in batch {
325 if let Some(t) = token {
326 if t.is_cancelled() {
327 return Err("Cancelled".to_string());
328 }
329 }
330 // Filter individual items if paths are provided
331 if let Some(filter_paths) = normalized_paths {
332 if !filter_paths.is_empty()
333 && !filter_paths.iter().any(|p| {
334 eval_key.starts_with(p.as_str())
335 || (p.starts_with(eval_key.as_str())
336 && !eval_key.contains("/$params/"))
337 })
338 {
339 continue;
340 }
341 }
342
343 let pointer_path =
344 path_utils::normalize_to_json_pointer(eval_key).into_owned();
345
346 // Cache miss - evaluate
347 let is_table = self.table_metadata.contains_key(eval_key);
348
349 if is_table {
350 let mut external_deps = indexmap::IndexSet::new();
351 let pointer_data_prefix = pointer_path.replace("/properties/", "/");
352 let pointer_data_prefix_slash = format!("{}/", pointer_data_prefix);
353 if let Some(deps) = self.dependencies.get(eval_key) {
354 for dep in deps {
355 let dep_data_path =
356 crate::jsoneval::path_utils::normalize_to_json_pointer(dep)
357 .replace("/properties/", "/");
358 if dep_data_path != pointer_data_prefix
359 && !dep_data_path.starts_with(&pointer_data_prefix_slash)
360 {
361 external_deps.insert(dep.clone());
362 }
363 }
364 }
365
366 #[cfg(debug_assertions)]
367 if external_deps.is_empty() {
368 if let Some(meta) = self.table_metadata.get(eval_key) {
369 if !meta.data_plans.is_empty() {
370 eprintln!(
371 "[jsoneval DEBUG] table {} has zero external_deps but \
372 non-empty data_plans — $params changes may not \
373 invalidate its cache",
374 eval_key
375 );
376 }
377 }
378 }
379
380 if let Some(cached_result) =
381 self.eval_cache.check_cache(eval_key, &external_deps)
382 {
383 let static_key = format!("/$table{}", pointer_path);
384 let arc_value = std::sync::Arc::new(cached_result.clone());
385
386 Arc::make_mut(&mut self.static_arrays)
387 .insert(static_key.clone(), std::sync::Arc::clone(&arc_value));
388
389 self.eval_data.set(&pointer_path, Value::clone(&arc_value));
390
391 let marker = serde_json::json!({ "$static_array": static_key });
392 if let Some(schema_value) =
393 self.evaluated_schema.pointer_mut(&pointer_path)
394 {
395 *schema_value = marker;
396 }
397 continue;
398 }
399
400 if let Ok(rows) = table_evaluate::evaluate_table(
401 self,
402 eval_key,
403 &eval_data_snapshot,
404 token,
405 ) {
406 let result_val = Value::Array(rows);
407 self.eval_cache.store_cache(
408 eval_key,
409 &external_deps,
410 result_val.clone(),
411 );
412
413 // NOTE: bump_params_version / bump_data_version for table results
414 // is now handled inside store_cache (conditional on value change).
415 // The separate bump here was double-counting: store_cache uses T2
416 // comparison while this block used eval_data as reference point,
417 // causing two version increments per changed table.
418
419 let static_key = format!("/$table{}", pointer_path);
420 let arc_value = std::sync::Arc::new(result_val);
421
422 Arc::make_mut(&mut self.static_arrays)
423 .insert(static_key.clone(), std::sync::Arc::clone(&arc_value));
424
425 self.eval_data.set(&pointer_path, Value::clone(&arc_value));
426
427 let marker = serde_json::json!({ "$static_array": static_key });
428 if let Some(schema_value) =
429 self.evaluated_schema.pointer_mut(&pointer_path)
430 {
431 *schema_value = marker;
432 }
433 }
434 } else {
435 let empty_deps = indexmap::IndexSet::new();
436 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
437 if let Some(cached_result) =
438 self.eval_cache.check_cache(eval_key, &deps)
439 {
440 // Must still populate eval_data out of cache so subsequent formulas
441 // referencing this path in the same iteration can read the exact value
442 self.eval_data.set(&pointer_path, cached_result.clone());
443 if let Some(schema_value) =
444 self.evaluated_schema.pointer_mut(&pointer_path)
445 {
446 *schema_value = cached_result;
447 }
448 continue;
449 }
450
451 if let Some(logic_id) = self.evaluations.get(eval_key) {
452 if let Ok(val) =
453 self.engine.run(logic_id, eval_data_snapshot.data())
454 {
455 let cleaned_val = clean_float_noise_scalar(val);
456 let data_path = pointer_path.replace("/properties/", "/");
457 self.eval_cache.store_cache(
458 eval_key,
459 &deps,
460 cleaned_val.clone(),
461 );
462
463 // Bump data_versions when non-$params field value changes.
464 // $params bumps are handled inside store_cache (conditional).
465 let old_val = self
466 .eval_data
467 .get(&data_path)
468 .cloned()
469 .unwrap_or(Value::Null);
470 if cleaned_val != old_val && !data_path.starts_with("/$params")
471 {
472 self.eval_cache.bump_data_version(&data_path);
473 }
474
475 self.eval_data.set(&pointer_path, cleaned_val.clone());
476 if let Some(schema_value) =
477 self.evaluated_schema.pointer_mut(&pointer_path)
478 {
479 *schema_value = cleaned_val;
480 }
481 }
482 }
483 }
484 }
485 }
486 });
487
488 // Drop lock before calling evaluate_others
489 drop(_lock);
490
491 // Mark generation stable so the next evaluate_internal call can detect whether
492 // any formula was actually re-stored (via bump_data/params_version) since this run.
493 self.eval_cache.mark_evaluated();
494
495 self.evaluate_others(paths, token, had_cache_miss);
496
497 Ok(())
498 })
499 }
500
501 pub(crate) fn evaluate_others(
502 &mut self,
503 paths: Option<&[String]>,
504 token: Option<&CancellationToken>,
505 had_cache_miss: bool,
506 ) {
507 if let Some(t) = token {
508 if t.is_cancelled() {
509 return;
510 }
511 }
512 time_block!(" evaluate_others()", {
513 // Step 1: Evaluate "rules" and "others" categories with caching
514 // Rules are evaluated here so their values are available in evaluated_schema
515 let combined_count = self.rules_evaluations.len() + self.others_evaluations.len();
516 if combined_count > 0 {
517 time_block!(" evaluate rules+others", {
518 let eval_data_snapshot = self.eval_data.clone();
519
520 let normalized_paths: Option<Vec<String>> = paths.map(|p_list| {
521 p_list
522 .iter()
523 .flat_map(|p| {
524 let ptr = path_utils::dot_notation_to_schema_pointer(p);
525 // Also support version with /properties/ prefix for root match
526 let with_props = if ptr.starts_with("#/") {
527 format!("#/properties/{}", &ptr[2..])
528 } else {
529 ptr.clone()
530 };
531 vec![ptr, with_props]
532 })
533 .collect()
534 });
535
536 // Sequential evaluation
537 let combined_evals: Vec<&String> = self
538 .rules_evaluations
539 .iter()
540 .chain(self.others_evaluations.iter())
541 .collect();
542
543 for eval_key in combined_evals {
544 if let Some(t) = token {
545 if t.is_cancelled() {
546 return;
547 }
548 }
549 // Filter items if paths are provided
550 if let Some(filter_paths) = normalized_paths.as_ref() {
551 if !filter_paths.is_empty()
552 && !filter_paths.iter().any(|p| {
553 eval_key.starts_with(p.as_str())
554 || (p.starts_with(eval_key.as_str())
555 && !eval_key.contains("/$params/"))
556 })
557 {
558 continue;
559 }
560 }
561
562 let pointer_path =
563 path_utils::normalize_to_json_pointer(eval_key).into_owned();
564 let empty_deps = indexmap::IndexSet::new();
565 let deps = self.dependencies.get(eval_key).unwrap_or(&empty_deps);
566
567 if let Some(cached_result) = self.eval_cache.check_cache(eval_key, &deps) {
568 if let Some(pointer_value) =
569 self.evaluated_schema.pointer_mut(&pointer_path)
570 {
571 if !pointer_path.starts_with("$")
572 && pointer_path.contains("/rules/")
573 && !pointer_path.ends_with("/value")
574 {
575 if let Some(pointer_obj) = pointer_value.as_object_mut() {
576 pointer_obj.remove("$evaluation");
577 pointer_obj
578 .insert("value".to_string(), cached_result.clone());
579 }
580 } else {
581 *pointer_value = cached_result.clone();
582 }
583 }
584 continue;
585 }
586 if let Some(logic_id) = self.evaluations.get(eval_key) {
587 if let Ok(val) = self.engine.run(logic_id, eval_data_snapshot.data()) {
588 let cleaned_val = clean_float_noise_scalar(val);
589 self.eval_cache
590 .store_cache(eval_key, &deps, cleaned_val.clone());
591
592 if let Some(pointer_value) =
593 self.evaluated_schema.pointer_mut(&pointer_path)
594 {
595 if !pointer_path.starts_with("$")
596 && pointer_path.contains("/rules/")
597 && !pointer_path.ends_with("/value")
598 {
599 match pointer_value.as_object_mut() {
600 Some(pointer_obj) => {
601 pointer_obj.remove("$evaluation");
602 pointer_obj
603 .insert("value".to_string(), cleaned_val);
604 }
605 None => continue,
606 }
607 } else {
608 *pointer_value = cleaned_val;
609 }
610 }
611 }
612 }
613 }
614 });
615 }
616 });
617
618 // Step 2: Evaluate options URL templates (handles {variable} patterns)
619 // Skip when all entries were cache hits — template inputs can't have changed.
620 if had_cache_miss {
621 time_block!(" evaluate_options_templates", {
622 self.evaluate_options_templates(paths);
623 });
624
625 // Step 3: Resolve layout logic (metadata injection, hidden propagation)
626 // Skip when no values changed — layout state is guaranteed identical.
627 time_block!(" resolve_layout", {
628 let _ = self.resolve_layout(false);
629 });
630 }
631 }
632
633 /// Evaluate options URL templates (handles {variable} patterns)
634 fn evaluate_options_templates(&mut self, paths: Option<&[String]>) {
635 // Use pre-collected options templates from parsing (Arc clone is cheap)
636 let templates_to_eval = self.options_templates.clone();
637
638 // Evaluate each template
639 for (path, template_str, params_path) in templates_to_eval.iter() {
640 // Filter items if paths are provided
641 // 'path' here is the schema path to the field (dot notation or similar, need to check)
642 // It seems to be schema pointer based on usage in other methods
643 if let Some(filter_paths) = paths {
644 if !filter_paths.is_empty()
645 && !filter_paths
646 .iter()
647 .any(|p| path.starts_with(p.as_str()) || p.starts_with(path.as_str()))
648 {
649 continue;
650 }
651 }
652
653 if let Some(params) = self.evaluated_schema.pointer(¶ms_path) {
654 if let Ok(evaluated) = self.evaluate_template(&template_str, params) {
655 if let Some(target) = self.evaluated_schema.pointer_mut(&path) {
656 *target = Value::String(evaluated);
657 }
658 }
659 }
660 }
661 }
662
663 /// Evaluate a template string like "api/users/{id}" with params
664 fn evaluate_template(&self, template: &str, params: &Value) -> Result<String, String> {
665 let mut result = template.to_string();
666
667 // Simple template evaluation: replace {key} with params.key
668 if let Value::Object(params_map) = params {
669 for (key, value) in params_map {
670 let placeholder = format!("{{{}}}", key);
671 if let Some(str_val) = value.as_str() {
672 result = result.replace(&placeholder, str_val);
673 } else {
674 // Convert non-string values to strings
675 result = result.replace(&placeholder, &value.to_string());
676 }
677 }
678 }
679
680 Ok(result)
681 }
682}