1use super::conflict;
14use super::spec::MutationSpec;
15use std::collections::{HashMap, HashSet};
16
17#[derive(Debug, Clone)]
19pub struct ParallelBlueprint {
20 pub mutations: Vec<MutationSpec>,
22
23 pub deps: DependencyGraph,
25
26 pub conflicts: Vec<Conflict>,
28}
29
30impl ParallelBlueprint {
31 pub fn new() -> Self {
33 Self {
34 mutations: vec![],
35 deps: DependencyGraph::new(),
36 conflicts: vec![],
37 }
38 }
39
40 pub fn from_mutations(mutations: Vec<MutationSpec>) -> Self {
42 let mut builder = BlueprintBuilder::new();
43 for spec in mutations {
44 builder.add(spec);
45 }
46 builder.build()
47 }
48
49 pub fn needs_escalation(&self) -> bool {
51 !self.conflicts.is_empty()
52 }
53
54 pub fn ready_set(&self, completed: &HashSet<usize>) -> Vec<usize> {
56 self.deps.ready_set(self.mutations.len(), completed)
57 }
58
59 pub fn parallelism(&self) -> f64 {
61 if self.mutations.is_empty() {
62 return 1.0;
63 }
64
65 let critical_path = self.deps.critical_path_length(self.mutations.len());
66 if critical_path == 0 {
67 self.mutations.len() as f64
68 } else {
69 self.mutations.len() as f64 / critical_path as f64
70 }
71 }
72
73 pub fn topological_levels(&self) -> Vec<Vec<usize>> {
75 self.deps.topological_levels(self.mutations.len())
76 }
77}
78
79impl Default for ParallelBlueprint {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85#[derive(Debug, Clone, Default)]
87pub struct DependencyGraph {
88 edges: HashMap<usize, Vec<usize>>,
90}
91
92impl DependencyGraph {
93 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn add_dependency(&mut self, from: usize, to: usize) {
99 self.edges.entry(from).or_default().push(to);
100 }
101
102 pub fn dependencies_of(&self, idx: usize) -> &[usize] {
104 self.edges.get(&idx).map(|v| v.as_slice()).unwrap_or(&[])
105 }
106
107 pub fn is_ready(&self, idx: usize, completed: &HashSet<usize>) -> bool {
109 self.dependencies_of(idx)
110 .iter()
111 .all(|dep| completed.contains(dep))
112 }
113
114 pub fn ready_set(&self, total: usize, completed: &HashSet<usize>) -> Vec<usize> {
116 (0..total)
117 .filter(|idx| !completed.contains(idx) && self.is_ready(*idx, completed))
118 .collect()
119 }
120
121 pub fn has_ordering(&self, indices: &[usize]) -> bool {
123 for &a in indices {
124 for &b in indices {
125 if a != b && self.depends_on(a, b) {
126 return true;
127 }
128 }
129 }
130 false
131 }
132
133 pub fn depends_on(&self, a: usize, b: usize) -> bool {
135 let mut visited = HashSet::new();
136 self.depends_on_recursive(a, b, &mut visited)
137 }
138
139 fn depends_on_recursive(&self, a: usize, b: usize, visited: &mut HashSet<usize>) -> bool {
140 if visited.contains(&a) {
141 return false;
142 }
143 visited.insert(a);
144
145 for &dep in self.dependencies_of(a) {
146 if dep == b || self.depends_on_recursive(dep, b, visited) {
147 return true;
148 }
149 }
150 false
151 }
152
153 pub fn critical_path_length(&self, total: usize) -> usize {
155 let mut memo: HashMap<usize, usize> = HashMap::new();
156
157 fn dfs(idx: usize, graph: &DependencyGraph, memo: &mut HashMap<usize, usize>) -> usize {
158 if let Some(&cached) = memo.get(&idx) {
159 return cached;
160 }
161
162 let max_dep = graph
163 .dependencies_of(idx)
164 .iter()
165 .map(|&dep| dfs(dep, graph, memo))
166 .max()
167 .unwrap_or(0);
168
169 let length = max_dep + 1;
170 memo.insert(idx, length);
171 length
172 }
173
174 (0..total)
175 .map(|idx| dfs(idx, self, &mut memo))
176 .max()
177 .unwrap_or(0)
178 }
179
180 pub fn topological_levels(&self, total: usize) -> Vec<Vec<usize>> {
182 let mut levels: Vec<Vec<usize>> = vec![];
183 let mut completed: HashSet<usize> = HashSet::new();
184
185 while completed.len() < total {
186 let ready = self.ready_set(total, &completed);
187 if ready.is_empty() {
188 break;
190 }
191
192 for &idx in &ready {
193 completed.insert(idx);
194 }
195 levels.push(ready);
196 }
197
198 levels
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct Conflict {
205 pub kind: ConflictKind,
207
208 pub involved: Vec<usize>,
210
211 pub question: String,
213}
214
215#[derive(Debug, Clone)]
217pub enum ConflictKind {
218 SameTarget { target: String },
220
221 OrderDependent { reason: String },
223
224 DeleteReference {
226 deleted: String,
227 referenced_by: String,
228 },
229}
230
231#[derive(Debug, Default)]
233pub struct BlueprintBuilder {
234 specs: Vec<MutationSpec>,
235 deps: DependencyGraph,
236}
237
238impl BlueprintBuilder {
239 pub fn new() -> Self {
240 Self::default()
241 }
242
243 pub fn add(&mut self, spec: MutationSpec) -> &mut Self {
245 self.specs.push(spec);
246 self
247 }
248
249 pub fn add_dependency(&mut self, from: usize, to: usize) -> &mut Self {
251 self.deps.add_dependency(from, to);
252 self
253 }
254
255 pub fn build(self) -> ParallelBlueprint {
257 let conflicts = detect_conflicts(&self.specs, &self.deps);
258
259 ParallelBlueprint {
260 mutations: self.specs,
261 deps: self.deps,
262 conflicts,
263 }
264 }
265}
266
267fn detect_conflicts(specs: &[MutationSpec], deps: &DependencyGraph) -> Vec<Conflict> {
269 let mut conflicts = vec![];
270
271 let conflicting_pairs = conflict::find_conflicting_pairs(specs);
273
274 for (i, j) in conflicting_pairs {
275 if deps.has_ordering(&[i, j]) {
277 continue;
278 }
279
280 conflicts.push(Conflict {
282 kind: ConflictKind::SameTarget {
283 target: format!("spec[{}] vs spec[{}]", i, j),
284 },
285 involved: vec![i, j],
286 question: format!(
287 "Specs {} and {} conflict without defined order. Which should be applied first?",
288 i, j
289 ),
290 });
291 }
292
293 conflicts
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::executor::spec::{MutationTargetSymbol, Scope};
300 use ryo_symbol::SymbolId;
301
302 fn dummy_id(index: u32) -> SymbolId {
304 SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
305 }
306
307 #[test]
308 fn test_dependency_graph_ready_set() {
309 let mut deps = DependencyGraph::new();
310 deps.add_dependency(1, 0);
314
315 let completed = HashSet::new();
316 let ready = deps.ready_set(3, &completed);
317 assert!(ready.contains(&0));
318 assert!(!ready.contains(&1)); assert!(ready.contains(&2));
320
321 let mut completed = HashSet::new();
322 completed.insert(0);
323 let ready = deps.ready_set(3, &completed);
324 assert!(ready.contains(&1)); assert!(ready.contains(&2));
326 }
327
328 #[test]
329 fn test_topological_levels() {
330 let mut deps = DependencyGraph::new();
331 deps.add_dependency(1, 0);
335 deps.add_dependency(3, 1);
336
337 let levels = deps.topological_levels(4);
338 assert_eq!(levels.len(), 3);
339 assert!(levels[0].contains(&0));
340 assert!(levels[0].contains(&2));
341 assert!(levels[1].contains(&1));
342 assert!(levels[2].contains(&3));
343 }
344
345 #[test]
346 fn test_same_target_conflict_detection() {
347 let specs = vec![
348 MutationSpec::ChangeVisibility {
349 target: MutationTargetSymbol::ById(dummy_id(1)),
350 visibility: crate::executor::spec::Visibility::Pub,
351 },
352 MutationSpec::AddDerive {
353 target: MutationTargetSymbol::ById(dummy_id(1)),
354 derives: vec!["Debug".to_string()],
355 },
356 ];
357
358 let blueprint = ParallelBlueprint::from_mutations(specs);
359 assert!(!blueprint.conflicts.is_empty());
361 }
362
363 #[test]
364 fn test_rename_chain_conflict() {
365 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
366
367 let mut registry = SymbolRegistry::new();
369 let path_a = SymbolPath::parse("test_crate::A").unwrap();
370 let symbol_a = registry.register(path_a, SymbolKind::Struct).unwrap();
371
372 let specs = vec![
373 MutationSpec::Rename {
374 target: MutationTargetSymbol::ById(symbol_a),
375 to: "B".to_string(),
376 scope: Scope::Project,
377 },
378 MutationSpec::Rename {
379 target: MutationTargetSymbol::ById(symbol_a),
380 to: "C".to_string(),
381 scope: Scope::Project,
382 },
383 ];
384
385 let blueprint = ParallelBlueprint::from_mutations(specs);
386 assert!(
388 !blueprint.conflicts.is_empty(),
389 "Expected conflict for two renames on same target"
390 );
391 }
392
393 #[test]
394 fn test_no_conflict_with_ordering() {
395 use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
396
397 let mut registry = SymbolRegistry::new();
399 let path_a = SymbolPath::parse("test_crate::A").unwrap();
400 let path_b = SymbolPath::parse("test_crate::B").unwrap();
401 let symbol_a = registry.register(path_a, SymbolKind::Struct).unwrap();
402 let _symbol_b = registry.register(path_b, SymbolKind::Struct).unwrap();
403
404 let mut builder = BlueprintBuilder::new();
405 builder.add(MutationSpec::Rename {
406 target: MutationTargetSymbol::ById(symbol_a),
407 to: "B".to_string(),
408 scope: Scope::Project,
409 });
410 builder.add(MutationSpec::Rename {
411 target: MutationTargetSymbol::ById(symbol_a),
412 to: "C".to_string(),
413 scope: Scope::Project,
414 });
415 builder.add_dependency(1, 0);
417
418 let blueprint = builder.build();
419 assert!(blueprint.conflicts.is_empty());
421 }
422
423 #[test]
424 fn test_delete_reference_conflict() {
425 use ryo_source::ItemKind;
426
427 let specs = vec![
428 MutationSpec::RemoveItem {
429 target: MutationTargetSymbol::ById(dummy_id(1)),
430 item_kind: ItemKind::Struct,
431 },
432 MutationSpec::AddDerive {
433 target: MutationTargetSymbol::ById(dummy_id(1)),
434 derives: vec!["Debug".to_string()],
435 },
436 ];
437
438 let blueprint = ParallelBlueprint::from_mutations(specs);
439 assert!(
441 !blueprint.conflicts.is_empty(),
442 "Expected conflict: RemoveItem + AddDerive on same target"
443 );
444 }
445
446 #[test]
447 fn test_visibility_conflict() {
448 use crate::executor::spec::Visibility;
449
450 let specs = vec![
451 MutationSpec::ChangeVisibility {
452 target: MutationTargetSymbol::ById(dummy_id(1)),
453 visibility: Visibility::Pub,
454 },
455 MutationSpec::ChangeVisibility {
456 target: MutationTargetSymbol::ById(dummy_id(1)),
457 visibility: Visibility::PubCrate,
458 },
459 ];
460
461 let blueprint = ParallelBlueprint::from_mutations(specs);
462 assert!(!blueprint.conflicts.is_empty());
464 }
465
466 #[test]
467 fn test_visibility_no_conflict_same_value() {
468 use crate::executor::spec::Visibility;
469
470 let specs = vec![
471 MutationSpec::ChangeVisibility {
472 target: MutationTargetSymbol::ById(dummy_id(1)),
473 visibility: Visibility::Pub,
474 },
475 MutationSpec::ChangeVisibility {
476 target: MutationTargetSymbol::ById(dummy_id(1)),
477 visibility: Visibility::Pub,
478 },
479 ];
480
481 let blueprint = ParallelBlueprint::from_mutations(specs);
482 let vis_conflicts: Vec<_> = blueprint
484 .conflicts
485 .iter()
486 .filter(|c| {
487 matches!(c.kind, ConflictKind::SameTarget { ref target } if target == "Config")
488 && c.question.contains("visibility")
489 })
490 .collect();
491 assert!(vis_conflicts.is_empty());
492 }
493}