Skip to main content

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}