ryo_source/parallel.rs
1//! Parallel access patterns for RustAST and PureFile.
2//!
3//! # Thread Safety Notes
4//!
5//! ## RustAST (syn-based)
6//!
7//! `syn::File` is NOT `Send`/`Sync` due to `proc_macro2::Span` containing
8//! raw pointers. This means:
9//!
10//! - Cannot share `RustAST` across threads with `Arc`
11//! - Cannot use `thread::spawn` with `RustAST`
12//!
13//! ## PureFile (span-free)
14//!
15//! `PureFile` is `Send + Sync` - fully thread-safe! This means:
16//!
17//! - Can share with `Arc<PureFile>` across threads
18//! - Can use rayon parallel iterators directly
19//! - Ideal for parallel code analysis
20//!
21//! ## Practical Patterns for Murmuration
22//!
23//! 1. **PureFile + Arc**: Share read-only AST across threads
24//! 2. **PureFile + COW**: Clone-and-modify for parallel mutations
25//! 3. **Parse-per-thread**: Each thread parses source independently
26//! 4. **Rayon for CPU-bound**: Use rayon for parallel iteration on collections
27
28use crate::ast::RustAST;
29use crate::ops::{Rename, RenameResult};
30
31/// Result of a mutation operation with Copy-on-Write semantics.
32#[derive(Debug)]
33pub struct MutResult<T> {
34 /// The mutated AST (new copy).
35 pub ast: RustAST,
36 /// The operation result.
37 pub result: T,
38}
39
40/// Copy-on-Write mutation patterns for safe AST manipulation.
41///
42/// These patterns ensure the original AST is never modified,
43/// making it safe to work with multiple "versions" of the code.
44pub struct CowMut;
45
46impl CowMut {
47 /// Apply a rename with Copy-on-Write semantics.
48 ///
49 /// Returns a new AST, leaving the original unchanged.
50 pub fn rename(ast: &RustAST, old: &str, new: &str) -> MutResult<RenameResult> {
51 let mut cloned = ast.clone();
52 let result = Rename::apply(&mut cloned, old, new);
53 MutResult {
54 ast: cloned,
55 result,
56 }
57 }
58
59 /// Apply multiple independent renames, each producing a new AST.
60 ///
61 /// Returns one result per rename operation.
62 pub fn multi_rename(ast: &RustAST, renames: &[(&str, &str)]) -> Vec<MutResult<RenameResult>> {
63 renames
64 .iter()
65 .map(|(old, new)| Self::rename(ast, old, new))
66 .collect()
67 }
68
69 /// Chain multiple renames on the same AST.
70 ///
71 /// Each rename is applied sequentially to the result of the previous.
72 pub fn chain_renames(ast: &RustAST, renames: &[(&str, &str)]) -> MutResult<Vec<RenameResult>> {
73 let mut current = ast.clone();
74 let mut results = Vec::with_capacity(renames.len());
75
76 for (old, new) in renames {
77 let result = Rename::apply(&mut current, old, new);
78 results.push(result);
79 }
80
81 MutResult {
82 ast: current,
83 result: results,
84 }
85 }
86}
87
88/// Parallel patterns using source strings (thread-safe).
89///
90/// Work with source code strings across threads, parse in each thread.
91pub struct SourceParallel;
92
93impl SourceParallel {
94 /// Parse and analyze source in parallel using rayon.
95 ///
96 /// Each source is parsed independently in the thread pool.
97 /// Note: RustAST is not Send, so parsing and analysis happen
98 /// together within each thread.
99 #[cfg(feature = "parallel")]
100 pub fn analyze_sources<F, T>(sources: &[&str], analyzer: F) -> Vec<T>
101 where
102 F: Fn(&RustAST) -> T + Sync,
103 T: Send,
104 {
105 use rayon::prelude::*;
106
107 // Parse and analyze in one step to avoid sending RustAST across threads
108 sources
109 .par_iter()
110 .filter_map(|src| {
111 let ast = RustAST::parse(src).ok()?;
112 Some(analyzer(&ast))
113 })
114 .collect()
115 }
116
117 /// Transform multiple sources in parallel.
118 ///
119 /// Returns (transformed_source, result) for each input.
120 /// Note: RustAST is not Send, so parsing and transformation happen
121 /// together within each thread.
122 #[cfg(feature = "parallel")]
123 pub fn transform_sources<F, T>(sources: &[&str], transformer: F) -> Vec<(String, T)>
124 where
125 F: Fn(&mut RustAST) -> T + Sync,
126 T: Send,
127 {
128 use rayon::prelude::*;
129
130 sources
131 .par_iter()
132 .filter_map(|src| {
133 let mut ast = RustAST::parse(src).ok()?;
134 let result = transformer(&mut ast);
135 Some((ast.to_string(), result))
136 })
137 .collect()
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_cow_rename() {
147 let original = RustAST::parse("fn foo() {}").unwrap();
148 let original_str = original.to_string();
149
150 // COW mutation
151 let result = CowMut::rename(&original, "foo", "bar");
152
153 // Original unchanged
154 assert_eq!(original.to_string(), original_str);
155
156 // New AST has the change
157 assert!(result.ast.to_string().contains("fn bar"));
158 assert_eq!(result.result.count, 1);
159 }
160
161 #[test]
162 fn test_multi_rename() {
163 let ast = RustAST::parse(
164 r#"
165 fn alpha() {}
166 fn beta() {}
167 fn gamma() {}
168 "#,
169 )
170 .unwrap();
171
172 let renames = vec![("alpha", "first"), ("beta", "second"), ("gamma", "third")];
173 let results = CowMut::multi_rename(&ast, &renames);
174
175 assert_eq!(results.len(), 3);
176
177 // Each result has only its rename applied (independent copies)
178 assert!(results[0].ast.to_string().contains("fn first"));
179 assert!(results[0].ast.to_string().contains("fn beta")); // others unchanged
180
181 assert!(results[1].ast.to_string().contains("fn second"));
182 assert!(results[1].ast.to_string().contains("fn alpha")); // others unchanged
183
184 assert!(results[2].ast.to_string().contains("fn third"));
185 assert!(results[2].ast.to_string().contains("fn alpha")); // others unchanged
186 }
187
188 #[test]
189 fn test_chain_renames() {
190 let ast = RustAST::parse(
191 r#"
192 fn alpha() {}
193 fn beta() {}
194 "#,
195 )
196 .unwrap();
197
198 let renames = vec![("alpha", "first"), ("beta", "second")];
199 let result = CowMut::chain_renames(&ast, &renames);
200
201 // Both renames applied to the final AST
202 assert!(result.ast.to_string().contains("fn first"));
203 assert!(result.ast.to_string().contains("fn second"));
204 assert!(!result.ast.to_string().contains("alpha"));
205 assert!(!result.ast.to_string().contains("beta"));
206
207 // Got results for each step
208 assert_eq!(result.result.len(), 2);
209 }
210}