json_eval_rs/topo_sort/
legacy.rs

1/// Topological sorting for legacy JSONEval
2
3use indexmap::{IndexMap, IndexSet};
4use crate::JSONEval;
5use crate::path_utils;
6use crate::topo_sort::common::{compute_parallel_batches, collect_transitive_deps};
7
8pub fn topological_sort(lib: &JSONEval) -> Result<Vec<Vec<String>>, String> {
9    let mut sorted = IndexSet::new();
10    let mut visited = IndexSet::new();
11    let mut visiting = IndexSet::new();
12
13    // Filter evaluations to exclude layout, rules, config, dependents, options, condition, value
14    let filtered_evaluations: IndexMap<String, IndexSet<String>> = lib
15        .evaluations
16        .keys()
17        .filter(|key| {
18            !key.contains("/dependents/")
19                && !key.contains("/rules/")
20                && !key.contains("/options/")
21                && !key.contains("/condition/")
22                && !key.contains("/$layout/")
23                && !key.contains("/config/")
24                && !key.contains("/items/")
25                && !key.ends_with("/options")
26                && !key.ends_with("/value")
27                && (key.starts_with("#/$") && !key.contains("/value/"))
28        })
29        .map(|key| {
30            let deps = lib.dependencies.get(key).cloned().unwrap_or_default();
31            (key.clone(), deps)
32        })
33        .collect();
34
35    // Group table evaluations and merge dependencies
36    let mut table_groups: IndexMap<String, IndexSet<String>> = IndexMap::new();
37    let mut evaluation_to_table: IndexMap<String, String> = IndexMap::new();
38
39    // First pass: identify all table paths from $table keys
40    let mut table_paths: IndexSet<String> = IndexSet::new();
41    for table_key in lib.tables.keys() {
42        // Extract table path by removing "/$table" suffix
43        let table_path = table_key.to_string();
44        table_paths.insert(table_path);
45    }
46
47    // Create a mapping of normalized names to table paths
48    let mut normalized_to_table: IndexMap<String, String> = IndexMap::new();
49    for tp in &table_paths {
50        // Extract the last segment (table name) for matching
51        if let Some(last_segment) = tp.rsplit('/').next() {
52            normalized_to_table.insert(last_segment.to_string(), tp.clone());
53        }
54    }
55
56    // Create a mapping from JSON pointer paths to evaluation keys for dependency resolution
57    let mut pointer_to_eval: IndexMap<String, String> = IndexMap::new();
58    for eval_key in filtered_evaluations.keys() {
59        // Convert evaluation keys to JSON pointers
60        let pointer = path_utils::normalize_to_json_pointer(eval_key);
61        pointer_to_eval.insert(pointer, eval_key.clone());
62    }
63    
64    // Also add table paths to pointer_to_eval for dependency resolution
65    for table_path in &table_paths {
66        let pointer = path_utils::normalize_to_json_pointer(table_path);
67        pointer_to_eval.insert(pointer, table_path.clone());
68    }
69
70    // Second pass: group ALL evaluations by table and merge dependencies
71    // Process ALL evaluations (not just filtered ones) to capture table dependencies
72    for (eval_key, deps) in lib.evaluations.keys().map(|k| {
73        let deps = lib.dependencies.get(k).cloned().unwrap_or_default();
74        (k, deps)
75    }) {
76        // Find which table this evaluation belongs to
77        // Use longest match to handle nested table names correctly
78        // (e.g., ILB_SURRENDER vs ILB_SURRENDER_BENPAY_CLONE)
79        let table_path_opt = table_paths
80            .iter()
81            .filter(|tp| eval_key.starts_with(tp.as_str()))
82            .max_by_key(|tp| tp.len());
83
84        if let Some(table_path) = table_path_opt {
85            evaluation_to_table.insert(eval_key.clone(), table_path.clone());
86
87            // Normalize dependencies to table paths where applicable
88            let normalized_deps: IndexSet<String> = deps
89                .iter()
90                .filter_map(|dep| {
91                    // Ignore self column dependencies (starts with $ and no dot/slash)
92                    if dep.starts_with('$') && !dep.contains('.') && !dep.contains('/') {
93                        return None;
94                    }
95
96                    // Check if dependency is a JSON pointer path that maps to an evaluation
97                    if let Some(eval_key) = pointer_to_eval.get(dep) {
98                        return Some(eval_key.clone());
99                    }
100
101                    // Check if dependency references another table path (flexible matching)
102                    for tp in &table_paths {
103                        let tp_str = tp.as_str();
104                        let tp_with_slash = format!("{}/", tp_str);
105
106                        // Match if:
107                        // 1. dep equals table path exactly (for static tables)
108                        // 2. dep starts with table path (for sub-fields like table.0.field)
109                        if tp_str != table_path.as_str() {
110                            if dep == tp_str || dep.starts_with(&tp_with_slash) {
111                                return Some(tp.clone());
112                            }
113                        }
114                    }
115
116                    // Check if dependency matches a normalized table name
117                    if let Some(target_table) = normalized_to_table.get(dep) {
118                        if target_table != table_path {
119                            return Some(target_table.clone());
120                        }
121                    }
122
123                    // Keep non-table dependencies as-is (but not self-table deps)
124                    let table_path_with_slash = format!("{}/", table_path.as_str());
125                    if !dep.starts_with(table_path.as_str())
126                        && !dep.starts_with(&table_path_with_slash)
127                    {
128                        Some(dep.clone())
129                    } else {
130                        None
131                    }
132                })
133                .collect();
134
135            table_groups
136                .entry(table_path.clone())
137                .or_insert_with(IndexSet::new)
138                .extend(normalized_deps);
139        }
140    }
141
142    // Create a unified graph and resolve JSON pointer dependencies in table groups
143    let mut unified_graph: IndexMap<String, IndexSet<String>> = IndexMap::new();
144
145    // Add table groups with resolved dependencies
146    for (table_path, deps) in &table_groups {
147        let resolved_deps: IndexSet<String> = deps
148            .iter()
149            .filter_map(|dep| {
150                // Filter out self-references (table depending on itself)
151                // This is common for iterative calculations within the same table
152                if dep == table_path {
153                    return None;
154                }
155                
156                // Try to resolve JSON pointer path to evaluation key
157                if let Some(eval_key) = pointer_to_eval.get(dep) {
158                    // Also filter out resolved self-references
159                    if eval_key == table_path {
160                        return None;
161                    }
162                    Some(eval_key.clone())
163                } else {
164                    // Keep as-is if not resolvable
165                    Some(dep.clone())
166                }
167            })
168            .collect();
169        unified_graph.insert(table_path.clone(), resolved_deps);
170    }
171
172    // Add non-table evaluations to the unified graph
173    for (eval_key, deps) in &filtered_evaluations {
174        if !evaluation_to_table.contains_key(eval_key) {
175            // Normalize dependencies for non-table evaluations
176            let mut normalized_deps: IndexSet<String> = IndexSet::new();
177            
178            for dep in deps {
179                // Check if dependency is a JSON pointer path that maps to an evaluation
180                if let Some(eval_key) = pointer_to_eval.get(dep) {
181                    normalized_deps.insert(eval_key.clone());
182                    continue;
183                }
184
185                // Check if dependency references a table/array path
186                let mut found_table = false;
187                for tp in &table_paths {
188                    let tp_str = tp.as_str();
189                    let tp_with_slash = format!("{}/", tp_str);
190
191                    // Match if:
192                    // 1. dep equals table path exactly (for static tables/arrays)
193                    // 2. dep starts with table path/ (for sub-fields)
194                    if dep == tp_str || dep.starts_with(&tp_with_slash) {
195                        normalized_deps.insert(tp.clone());
196                        found_table = true;
197                        break;
198                    }
199                }
200                
201                if found_table {
202                    continue;
203                }
204                
205                // OPTIMIZED: Check if dependency is a static array with evaluated fields
206                // Use consistent path utilities for conversion
207                let dep_as_pointer = path_utils::normalize_to_json_pointer(dep);
208                let dep_as_eval_prefix = format!("#{}", dep_as_pointer);
209                let has_field_evaluations = lib.evaluations.keys().any(|k| {
210                    k.starts_with(&dep_as_eval_prefix) 
211                    && k.len() > dep_as_eval_prefix.len()
212                    && k[dep_as_eval_prefix.len()..].starts_with('/')
213                });
214                
215                if has_field_evaluations {
216                    // Add all field evaluations as dependencies
217                    for field_eval_key in lib.evaluations.keys() {
218                        if field_eval_key.starts_with(&dep_as_eval_prefix) 
219                            && field_eval_key.len() > dep_as_eval_prefix.len()
220                            && field_eval_key[dep_as_eval_prefix.len()..].starts_with('/') {
221                            normalized_deps.insert(field_eval_key.clone());
222                        }
223                    }
224                } else {
225                    normalized_deps.insert(dep.clone());
226                }
227            }
228
229            unified_graph.insert(eval_key.clone(), normalized_deps);
230        }
231    }
232
233    // ==========================================
234    // 3-PHASE PROCESSING: Dependencies → Tables → Rest
235    // ==========================================
236    
237    // Identify all table dependencies (transitive)
238    // This includes all non-table nodes that tables transitively depend on
239    let mut table_dependencies = IndexSet::new();
240    for table_path in &table_paths {
241        if let Some(deps) = unified_graph.get(table_path) {
242            collect_transitive_deps(
243                deps,
244                &unified_graph,
245                &table_paths,
246                &mut table_dependencies,
247            );
248        }
249    }
250    
251    // CRITICAL: Expand to complete transitive closure
252    // Ensure ALL non-table dependencies of phase 1 nodes are also in phase 1
253    // Example: If table depends on A, and A depends on B, then both A and B are in phase 1
254    let mut expanded = true;
255    while expanded {
256        expanded = false;
257        let current_deps: Vec<String> = table_dependencies.iter().cloned().collect();
258        for dep in &current_deps {
259            if let Some(sub_deps) = unified_graph.get(dep) {
260                for sub_dep in sub_deps {
261                    // Skip tables - they stay in phase 2
262                    if !table_paths.contains(sub_dep) {
263                        if table_dependencies.insert(sub_dep.clone()) {
264                            expanded = true; // Found new dependency, need another pass
265                        }
266                    }
267                }
268            }
269        }
270    }
271    
272    // Separate nodes into phases
273    let mut phase1_nodes = Vec::new(); // Table dependencies (non-tables needed by tables)
274    let mut phase2_nodes = Vec::new(); // Tables
275    let mut phase3_nodes = Vec::new(); // Everything else
276    
277    for node in unified_graph.keys() {
278        if table_paths.contains(node) {
279            // Phase 2: Tables
280            phase2_nodes.push(node.clone());
281        } else if table_dependencies.contains(node) {
282            // Phase 1: Non-table dependencies of tables
283            phase1_nodes.push(node.clone());
284        } else {
285            // Phase 3: Remaining nodes
286            phase3_nodes.push(node.clone());
287        }
288    }
289    
290    // Sort phase 1 and phase 3 by dependency order (nodes with fewer deps first)
291    // This provides a better starting order for topological processing
292    let sort_by_deps = |a: &String, b: &String| {
293        let a_deps = unified_graph.get(a).map(|d| d.len()).unwrap_or(0);
294        let b_deps = unified_graph.get(b).map(|d| d.len()).unwrap_or(0);
295        a_deps.cmp(&b_deps).then_with(|| a.cmp(b))
296    };
297    
298    phase1_nodes.sort_by(sort_by_deps);
299    phase3_nodes.sort_by(sort_by_deps);
300    
301    // PHASE 1: Process table dependencies (respecting their internal dependencies)
302    for node in &phase1_nodes {
303        if !visited.contains(node) {
304            let deps = unified_graph.get(node).cloned().unwrap_or_default();
305            // visit_node will recursively process dependencies in correct order
306            visit_node(lib, node, &deps, &unified_graph, &mut visited, &mut visiting, &mut sorted)?;
307        }
308    }
309    
310    // PHASE 2: Process tables in dependency order
311    // Sort tables by their dependencies (tables with fewer/no table deps come first)
312    phase2_nodes.sort_by(|a, b| {
313        let a_deps = unified_graph.get(a).map(|d| d.len()).unwrap_or(0);
314        let b_deps = unified_graph.get(b).map(|d| d.len()).unwrap_or(0);
315        
316        // Check if A depends on B or B depends on A
317        let a_deps_on_b = unified_graph.get(a)
318            .map(|deps| deps.contains(b))
319            .unwrap_or(false);
320        let b_deps_on_a = unified_graph.get(b)
321            .map(|deps| deps.contains(a))
322            .unwrap_or(false);
323        
324        if a_deps_on_b {
325            std::cmp::Ordering::Greater // A depends on B, so B comes first
326        } else if b_deps_on_a {
327            std::cmp::Ordering::Less // B depends on A, so A comes first
328        } else {
329            // No direct dependency, sort by dependency count then alphabetically
330            a_deps.cmp(&b_deps).then_with(|| a.cmp(b))
331        }
332    });
333    
334    for node in &phase2_nodes {
335        if !visited.contains(node) {
336            let deps = unified_graph.get(node).cloned().unwrap_or_default();
337            visit_node(lib, node, &deps, &unified_graph, &mut visited, &mut visiting, &mut sorted)?;
338        }
339    }
340    
341    // PHASE 3: Process remaining nodes (respecting their internal dependencies)
342    for node in &phase3_nodes {
343        if !visited.contains(node) {
344            let deps = unified_graph.get(node).cloned().unwrap_or_default();
345            // visit_node will recursively process dependencies in correct order
346            visit_node(lib, node, &deps, &unified_graph, &mut visited, &mut visiting, &mut sorted)?;
347        }
348    }
349
350    // Now convert the flat sorted list into parallel batches
351    // Batch nodes by their "level" - all nodes at the same level can run in parallel
352    let batches = compute_parallel_batches(&sorted, &unified_graph, &table_paths);
353    
354    Ok(batches)
355}
356
357/// Compute parallel execution batches from a topologically sorted list
358/// 
359/// Algorithm: Assign each node to the earliest batch where all its dependencies
360/// have been processed in previous batches.
361
362pub fn visit_node_with_priority(
363    lib: &JSONEval,
364    node: &str,
365    deps: &IndexSet<String>,
366    graph: &IndexMap<String, IndexSet<String>>,
367    visited: &mut IndexSet<String>,
368    visiting: &mut IndexSet<String>,
369    sorted: &mut IndexSet<String>,
370    table_paths: &IndexSet<String>,
371) -> Result<(), String> {
372    if visiting.contains(node) {
373        return Err(format!("Circular dependency detected involving: {}", node));
374    }
375
376    if visited.contains(node) {
377        return Ok(());
378    }
379
380    visiting.insert(node.to_string());
381
382    // Sort dependencies by priority: non-tables first
383    let mut sorted_deps: Vec<String> = deps.iter().cloned().collect();
384    sorted_deps.sort_by(|a, b| {
385        let a_is_table = table_paths.contains(a);
386        let b_is_table = table_paths.contains(b);
387        
388        match (a_is_table, b_is_table) {
389            (false, true) => std::cmp::Ordering::Less,  // non-table before table
390            (true, false) => std::cmp::Ordering::Greater, // table after non-table
391            _ => a.cmp(b), // same priority, sort alphabetically
392        }
393    });
394
395    // Process dependencies in priority order
396    for dep in sorted_deps {
397        if let Some(dep_deps) = graph.get(&dep) {
398            visit_node_with_priority(lib, &dep, dep_deps, graph, visited, visiting, sorted, table_paths)?;
399        }
400    }
401
402    visiting.swap_remove(node);
403    visited.insert(node.to_string());
404    sorted.insert(node.to_string());
405
406    Ok(())
407}
408
409pub fn visit_node(
410    lib: &JSONEval,
411    node: &str,
412    deps: &IndexSet<String>,
413    graph: &IndexMap<String, IndexSet<String>>,
414    visited: &mut IndexSet<String>,
415    visiting: &mut IndexSet<String>,
416    sorted: &mut IndexSet<String>,
417) -> Result<(), String> {
418    if visiting.contains(node) {
419        return Err(format!("Circular dependency detected involving: {}", node));
420    }
421
422    if visited.contains(node) {
423        return Ok(());
424    }
425
426    visiting.insert(node.to_string());
427
428    for dep in deps {
429        if let Some(dep_deps) = graph.get(dep) {
430            visit_node(lib, dep, dep_deps, graph, visited, visiting, sorted)?;
431        }
432    }
433
434    visiting.swap_remove(node);
435    visited.insert(node.to_string());
436    sorted.insert(node.to_string());
437
438    Ok(())
439}
440