use super::blueprint::ParallelBlueprint;
use super::registry::MutationRegistry;
use super::spec::{MutationSpec, MutationTargetSymbol, StmtInsertPosition};
use crate::engine::{collect_affected_ids, MutationEvent};
use ryo_analysis::{AnalysisContext, RegistryUpdateBatch, SymbolPath};
use ryo_mutations::MutationResult;
use ryo_source::pure::ToSynError;
use ryo_symbol::{MetadataError, SymbolId, WorkspaceFilePath};
use std::collections::HashSet;
use std::sync::Arc;
use tracing::{debug, info, instrument, warn};
#[derive(Debug, thiserror::Error)]
pub enum SyncError {
#[error("cargo metadata unavailable: {0}")]
Metadata(#[from] MetadataError),
#[error("source generation failed: {0}")]
SourceGeneration(#[from] ToSynError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExecutionStrategy {
#[default]
Sequential,
Wavefront,
}
pub fn suggest_strategy(blueprint: &ParallelBlueprint) -> ExecutionStrategy {
let parallelism = blueprint.parallelism();
let mutation_count = blueprint.mutations.len();
if parallelism <= 1.2 || mutation_count <= 2 {
ExecutionStrategy::Sequential
} else {
ExecutionStrategy::Wavefront
}
}
#[derive(Debug, Clone)]
pub struct BlueprintResult {
pub results: Vec<SpecResult>,
pub total_changes: usize,
pub modified_files: Vec<WorkspaceFilePath>,
pub success: bool,
pub error: Option<String>,
pub registry_updates: RegistryUpdateBatch,
}
#[derive(Debug, Clone)]
pub struct SpecResult {
pub index: usize,
pub spec_type: String,
pub changes: usize,
pub affected_files: Vec<WorkspaceFilePath>,
pub affected_symbols: Vec<SymbolPath>,
pub success: bool,
pub error: Option<String>,
pub registry_updates: RegistryUpdateBatch,
pub events: Vec<MutationEvent>,
}
impl BlueprintResult {
pub fn success(results: Vec<SpecResult>, modified_files: Vec<WorkspaceFilePath>) -> Self {
let total_changes = results.iter().map(|r| r.changes).sum();
let mut all_updates = RegistryUpdateBatch::new();
for result in &results {
for update in &result.registry_updates {
all_updates.push(update.clone());
}
}
Self {
results,
total_changes,
modified_files,
success: true,
error: None,
registry_updates: all_updates,
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
results: vec![],
total_changes: 0,
modified_files: vec![],
success: false,
error: Some(error.into()),
registry_updates: RegistryUpdateBatch::new(),
}
}
}
macro_rules! try_resolve {
($self:expr, $target:expr, $ctx:expr, $index:expr, $spec_type:expr, $affected_symbols:expr, $msg:expr $(,)?) => {
match $self.resolve_target_symbol_simple($target, $ctx) {
Ok(id) => id,
Err(e) => {
return SpecResult {
index: $index,
spec_type: $spec_type.clone(),
success: false,
changes: 0,
affected_files: vec![],
affected_symbols: $affected_symbols.clone(),
error: Some(format!("{}: {}", $msg, e)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
}
};
}
#[derive(Debug)]
pub struct BlueprintExecutor {
registry: MutationRegistry,
pub strategy: ExecutionStrategy,
pub verify_after_each: bool,
pub stop_on_error: bool,
pub ignore_conflicts: bool,
}
impl Default for BlueprintExecutor {
fn default() -> Self {
Self {
registry: MutationRegistry::default(),
strategy: ExecutionStrategy::default(),
verify_after_each: false,
stop_on_error: true,
ignore_conflicts: true,
}
}
}
impl BlueprintExecutor {
pub fn new() -> Self {
Self::default()
}
fn resolve_target_symbol_simple(
&self,
target: &super::spec::MutationTargetSymbol,
ctx: &AnalysisContext,
) -> Result<SymbolId, String> {
use super::spec::MutationTargetSymbol;
match target {
MutationTargetSymbol::ById(id) => {
if ctx.registry.resolve(*id).is_none() {
return Err(format!("Symbol {:?} not found in registry", id));
}
Ok(*id)
}
MutationTargetSymbol::ByPath(path) => {
ctx.registry
.iter()
.find(|(_, p)| *p == &**path)
.map(|(id, _)| id)
.ok_or_else(|| format!("Symbol at path '{}' not found", path))
}
MutationTargetSymbol::ByKindAndName(kind, name) => {
let normalized_name = normalize_generic_name(name);
let matches: Vec<_> = ctx
.registry
.iter()
.filter(|(_, path)| normalize_generic_name(path.name()) == normalized_name)
.collect();
match matches.len() {
0 => Err(format!("Symbol not found: kind={:?}, name={}", kind, name)),
1 => Ok(matches[0].0),
_ => Err(format!(
"Multiple symbols found: kind={:?}, name={} (found {} matches)",
kind,
name,
matches.len()
)),
}
}
MutationTargetSymbol::ByAffectedId {
parent_id,
kind,
name,
} => {
let parent_path = ctx
.registry
.resolve(*parent_id)
.ok_or_else(|| format!("Parent SymbolId {:?} not found", parent_id))?;
if let Some(child_name) = name {
parent_path
.child(child_name)
.map_err(|e| format!("Invalid child path: {}", e))
.and_then(|child_path| {
ctx.registry
.iter()
.find(|(_, p)| p == &&child_path)
.map(|(id, _)| id)
.ok_or_else(|| {
format!(
"Child symbol not found: parent={:?}, kind={:?}, name={}",
parent_id, kind, child_name
)
})
})
} else {
Err(format!(
"Anonymous child symbols not yet supported: parent={:?}, kind={:?}",
parent_id, kind
))
}
}
}
}
#[instrument(skip(self, blueprint, ctx), fields(mutations = blueprint.mutations.len()))]
pub fn execute_v2(
&self,
blueprint: &ParallelBlueprint,
ctx: &mut AnalysisContext,
) -> BlueprintResult {
debug!(
"Starting blueprint execution with {} mutations",
blueprint.mutations.len()
);
if !self.ignore_conflicts && blueprint.needs_escalation() {
warn!(
"Blueprint has {} conflicts requiring escalation",
blueprint.conflicts.len()
);
return BlueprintResult::failure(format!(
"Blueprint has {} conflicts requiring escalation",
blueprint.conflicts.len()
));
}
let mut results: Vec<SpecResult> = Vec::new();
let mut completed: HashSet<usize> = HashSet::new();
while completed.len() < blueprint.mutations.len() {
let ready = blueprint
.deps
.ready_set(blueprint.mutations.len(), &completed);
if ready.is_empty() {
if completed.len() < blueprint.mutations.len() {
warn!("Dependency cycle detected");
return BlueprintResult::failure("Dependency cycle detected");
}
break;
}
debug!("Ready to execute {} specs", ready.len());
for idx in ready {
let spec = &blueprint.mutations[idx];
debug!("Executing spec {}: {:?}", idx, spec);
let result = self.execute_spec_v2(idx, spec, ctx);
if let Some(ref error) = result.error {
warn!(
"Spec {} completed with error: success={}, changes={}, error={}",
idx, result.success, result.changes, error
);
} else {
debug!(
"Spec {} completed: success={}, changes={}, events={}",
idx,
result.success,
result.changes,
result.events.len()
);
}
let success = result.success;
results.push(result);
completed.insert(idx);
if !success && self.stop_on_error {
let total_changes = results.iter().map(|r| r.changes).sum();
let spec_error = results
.last()
.and_then(|r| r.error.as_ref())
.map(|e| format!("Spec {} failed: {}", idx, e))
.unwrap_or_else(|| format!("Stopped at spec {} (no error message)", idx));
warn!("Stopping on error at spec {}: {}", idx, spec_error);
return BlueprintResult {
results,
total_changes,
modified_files: Vec::new(),
success: false,
error: Some(spec_error),
registry_updates: RegistryUpdateBatch::new(),
};
}
}
}
let total_changes: usize = results.iter().map(|r| r.changes).sum();
info!(
"Blueprint execution completed: {} results, {} total changes",
results.len(),
total_changes
);
BlueprintResult::success(results, Vec::new())
}
#[instrument(skip(result, ctx), fields(total_changes = result.total_changes))]
pub fn sync_files_and_rebuild(
result: &BlueprintResult,
ctx: &mut AnalysisContext,
) -> Result<Vec<WorkspaceFilePath>, SyncError> {
use crate::engine::{collect_modified_symbols, RegistryGenerator};
use ryo_symbol::CargoMetadataProvider;
debug!(
"Starting sync_files_and_rebuild with {} results",
result.results.len()
);
let all_events: Vec<MutationEvent> = result
.results
.iter()
.flat_map(|r| r.events.clone())
.collect();
debug!(
"Collected {} mutation events from results",
all_events.len()
);
if all_events.is_empty() {
debug!("No mutation events — skipping file sync entirely");
return Ok(Vec::new());
}
let modified_symbol_ids = collect_modified_symbols(&all_events, ctx.registry());
debug!("Found {} modified symbols", modified_symbol_ids.len());
let metadata = CargoMetadataProvider::from_directory(&ctx.workspace_root)?;
let generator = RegistryGenerator::multi_file();
let workspace = generator.generate_affected(
&ctx.ast_registry,
ctx.registry(),
&modified_symbol_ids,
&metadata,
)?;
debug!(
"Generator produced {} crates with {} total files",
workspace.crates.len(),
workspace.total_files()
);
let mut modified_files = Vec::new();
for generated_crate in workspace.crates.values() {
debug!(
"Processing crate {} with {} files",
generated_crate.crate_name,
generated_crate.files.len()
);
use ryo_symbol::{CrateName, WorkspacePathResolver};
let crate_name = CrateName::new(&generated_crate.crate_name).expect(
"generator-emitted crate names must already satisfy CrateName validation; \
reaching this expect means the generator is producing invalid names",
);
let layout = metadata.crate_layout(&crate_name);
for (crate_relative_path, generated_file) in &generated_crate.files {
let workspace_relative = match &layout {
Some(layout) => layout.to_workspace_relative(crate_relative_path),
None => {
std::path::PathBuf::from(crate_relative_path.as_str())
}
};
let workspace_file = ctx
.files()
.keys()
.find(|wfp| {
wfp.crate_name().as_str() == generated_crate.crate_name
&& wfp.as_relative() == workspace_relative.as_path()
})
.cloned();
let wfp = if let Some(existing) = workspace_file {
debug!(
"Updating existing file: {} ({})",
crate_relative_path,
existing.as_relative().display()
);
existing
} else {
debug!(
"Creating new file: {} -> {} for crate {}",
crate_relative_path,
workspace_relative.display(),
generated_crate.crate_name
);
let resolver =
WorkspacePathResolver::new(ctx.workspace_root.as_ref().to_path_buf());
resolver.resolve_relative_with_crate(&workspace_relative, crate_name.clone())
};
let parsed = ryo_source::pure::PureFile::from_source(&generated_file.source);
let pure_file = parsed.unwrap_or_else(|_e| ryo_source::pure::PureFile::new());
ctx.files_mut().insert(wfp.clone(), Arc::new(pure_file));
modified_files.push(wfp);
}
}
debug!(
"Updated {} files in context from generator output",
modified_files.len()
);
if !all_events.is_empty() {
let affected_ids = collect_affected_ids(&all_events, ctx.registry());
debug!("Rebuilding with {} affected symbol IDs", affected_ids.len());
ctx.rebuild_after_mutation_by_symbols(&affected_ids);
} else if !modified_files.is_empty() {
debug!(
"Rebuilding with {} modified files (fallback)",
modified_files.len()
);
ctx.rebuild_after_mutation(&modified_files);
}
info!(
"sync_files_and_rebuild completed: {} files modified",
modified_files.len()
);
Ok(modified_files)
}
fn execute_spec_v2(
&self,
index: usize,
spec: &MutationSpec,
ctx: &mut AnalysisContext,
) -> SpecResult {
use crate::engine::{ASTMutationEngine, ExecutionResult};
use crate::executor::registry::ConvertError;
use ryo_mutations::basic::{
AddDeriveMutation, AddFieldMutation, RemoveDeriveMutation, RemoveFieldMutation,
RemoveModMutation,
};
let spec_type = spec_type_name(spec);
let affected_symbols: Vec<SymbolPath> = spec
.get_targets()
.iter()
.filter_map(|target| target.to_path(ctx.registry()))
.collect();
match self.registry.convert_v2(spec, ctx) {
Ok(mutations) => {
let exec_result = ASTMutationEngine::execute_ast_reg_batch_dyn(mutations, ctx);
if let MutationSpec::AddItem {
target, content, ..
} = spec
{
if let MutationTargetSymbol::ByPath(path) = target {
register_item_from_content(ctx, path, content);
} else if let MutationTargetSymbol::ById(id) = target {
if let Some(path) = ctx.registry().resolve(*id).cloned() {
register_item_from_content(ctx, &path, content);
}
}
}
return SpecResult {
index,
spec_type,
success: true,
changes: exec_result.result.changes,
affected_files: vec![], affected_symbols,
error: None,
registry_updates: RegistryUpdateBatch::new(),
events: exec_result.events,
};
}
Err(ConvertError::V2NotSupported) => {
}
Err(e) => {
return SpecResult {
index,
spec_type,
success: false,
changes: 0,
affected_files: vec![],
affected_symbols,
error: Some(format!("convert_v2 failed: {}", e)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
}
let exec_result = match spec {
MutationSpec::AddField {
target,
field_name,
field_type,
visibility,
..
} => {
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mut mutation = AddFieldMutation::new(symbol_id, field_name, field_type);
if matches!(visibility, super::spec::Visibility::Pub) {
mutation = mutation.public();
}
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveField {
target, field_name, ..
} => {
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = RemoveFieldMutation::new(symbol_id, field_name);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveMod {
target, mod_name, ..
} => {
use ryo_symbol::SymbolKind;
let parent_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve parent module"
);
let parent_path = match ctx.registry.resolve(parent_id) {
Some(p) => p,
None => {
return SpecResult {
index,
spec_type: spec_type.clone(),
success: false,
changes: 0,
affected_files: vec![],
affected_symbols: affected_symbols.clone(),
error: Some(format!(
"Parent module path not found for SymbolId {:?}",
parent_id
)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
};
let mod_path = match parent_path.child(mod_name) {
Ok(p) => p,
Err(e) => {
return SpecResult {
index,
spec_type: spec_type.clone(),
success: false,
changes: 0,
affected_files: vec![],
affected_symbols: affected_symbols.clone(),
error: Some(format!(
"Failed to build module path for '{}': {}",
mod_name, e
)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
};
let module_id = match ctx.registry.lookup(&mod_path) {
Some(id) => id,
None => {
return SpecResult {
index,
spec_type: spec_type.clone(),
success: false,
changes: 0,
affected_files: vec![],
affected_symbols: affected_symbols.clone(),
error: Some(format!(
"Module '{}' not found in {}",
mod_name, parent_path
)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
};
if ctx.registry.kind(module_id) != Some(SymbolKind::Mod) {
return SpecResult {
index,
spec_type: spec_type.clone(),
success: false,
changes: 0,
affected_files: vec![],
affected_symbols: affected_symbols.clone(),
error: Some(format!("Symbol {} is not a module", module_id)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
let mutation = RemoveModMutation::new(module_id);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::AddDerive {
target, derives, ..
} => {
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = AddDeriveMutation::new(symbol_id, derives.clone());
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveDerive {
target, derives, ..
} => {
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = RemoveDeriveMutation::new(symbol_id, derives.clone());
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::AddVariant {
target,
variant_name,
variant_kind,
..
} => {
use ryo_mutations::basic::AddVariantMutation;
use ryo_source::pure::{PureField, PureFields, PureType, PureVis};
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let fields = match variant_kind {
super::spec::VariantKind::Unit => PureFields::Unit,
super::spec::VariantKind::Tuple { types } => {
PureFields::Tuple(types.iter().map(|t| PureType::Path(t.clone())).collect())
}
super::spec::VariantKind::Struct { fields } => {
let pure_fields: Vec<PureField> = fields
.iter()
.map(|(n, t)| PureField {
attrs: Vec::new(),
vis: PureVis::Private,
name: n.clone(),
ty: PureType::Path(t.clone()),
})
.collect();
PureFields::Named(pure_fields)
}
};
let mutation = AddVariantMutation::new(symbol_id, variant_name, fields);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveVariant {
target,
variant_name,
..
} => {
use ryo_mutations::basic::RemoveVariantMutation;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = RemoveVariantMutation::new(symbol_id, variant_name);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ChangeVisibility {
target, visibility, ..
} => {
use ryo_mutations::basic::ChangeVisibilityMutation;
use ryo_source::pure::PureVis;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let pure_vis = match visibility {
super::spec::Visibility::Private => PureVis::Private,
super::spec::Visibility::Pub => PureVis::Public,
super::spec::Visibility::PubCrate => PureVis::Crate,
super::spec::Visibility::PubSuper => PureVis::Super,
super::spec::Visibility::PubIn(_) => PureVis::Public, };
let mutation = ChangeVisibilityMutation::new(symbol_id, pure_vis);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::Rename { target, to, .. } => {
use ryo_mutations::basic::RenameMutation;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = RenameMutation::new(symbol_id, to);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::AddMatchArm {
target,
enum_name,
pattern,
body,
} => {
use ryo_mutations::basic::AddMatchArmMutation;
let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
Ok(id) => id,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(e),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let mutation = AddMatchArmMutation::new(fn_id, enum_name, pattern, body);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveMatchArm {
target,
enum_name,
pattern,
} => {
use ryo_mutations::basic::RemoveMatchArmMutation;
let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
Ok(id) => id,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(e),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let mutation = RemoveMatchArmMutation::new(fn_id, enum_name, pattern);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ReplaceMatchArm {
target,
enum_name,
old_pattern,
new_pattern,
new_body,
} => {
use ryo_mutations::basic::ReplaceMatchArmMutation;
let fn_id = match self.resolve_target_symbol_simple(target, ctx) {
Ok(id) => id,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(e),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let mutation = ReplaceMatchArmMutation::new(
fn_id,
enum_name,
old_pattern,
new_pattern,
new_body,
);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::AddStructLiteralField {
target,
field_name,
value,
..
} => {
use ryo_mutations::basic::AddStructLiteralFieldMutation;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = AddStructLiteralFieldMutation::new(symbol_id, field_name, value);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveStructLiteralField {
target, field_name, ..
} => {
use ryo_mutations::basic::RemoveStructLiteralFieldMutation;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mutation = RemoveStructLiteralFieldMutation::new(symbol_id, field_name);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::OrganizeImports {
deduplicate,
merge_groups,
..
} => {
use ryo_mutations::idiom::OrganizeImportsMutation;
let mutation = OrganizeImportsMutation::new()
.with_deduplicate(*deduplicate)
.with_merge_groups(*merge_groups);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::AssignOp { .. } => {
use ryo_mutations::idiom::AssignOpMutation;
let mutation = AssignOpMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::BoolSimplify { .. } => {
use ryo_mutations::idiom::BoolSimplifyMutation;
let mutation = BoolSimplifyMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ComparisonToMethod { .. } => {
use ryo_mutations::idiom::ComparisonToMethodMutation;
let mutation = ComparisonToMethodMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::CollapsibleIf { .. } => {
use ryo_mutations::idiom::CollapsibleIfMutation;
let mutation = CollapsibleIfMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RedundantClosure { .. } => {
use ryo_mutations::idiom::RedundantClosureMutation;
let mutation = RedundantClosureMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::FilterNext { .. } => {
use ryo_mutations::idiom::FilterNextMutation;
let mutation = FilterNextMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::MapUnwrapOr { .. } => {
use ryo_mutations::idiom::MapUnwrapOrMutation;
let mutation = MapUnwrapOrMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::CloneOnCopy { .. } => {
use ryo_mutations::idiom::CloneOnCopyMutation;
let mutation = CloneOnCopyMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::LoopToIterator { .. } => {
use ryo_mutations::idiom::LoopToIteratorMutation;
let mutation = LoopToIteratorMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::UnwrapToQuestion { .. } => {
use ryo_mutations::idiom::UnwrapToQuestionMutation;
let mutation = UnwrapToQuestionMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ManualMap { .. } => {
use ryo_mutations::idiom::ManualMapMutation;
let mutation = ManualMapMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::MatchToIfLet { .. } => {
use ryo_mutations::idiom::MatchToIfLetMutation;
let mutation = MatchToIfLetMutation::new();
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::IntroduceVariable { expr, var_name, .. } => {
use ryo_mutations::idiom::IntroduceVariableMutation;
use ryo_source::ToPure;
let pure_expr = syn::parse_str::<syn::Expr>(expr)
.map(|e| e.to_pure())
.unwrap_or_else(|_| ryo_source::pure::PureExpr::Path(expr.clone()));
let mutation = IntroduceVariableMutation::new(pure_expr, var_name);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ExtractTrait {
target,
ref trait_name,
ref methods,
..
} => {
use ryo_mutations::basic::ExtractTraitMutation;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mut mutation = ExtractTraitMutation::new(symbol_id, trait_name.clone());
if let Some(ref m) = methods {
mutation = mutation.with_methods(m.clone());
}
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::InlineTrait {
target,
ref struct_name,
remove_trait,
..
} => {
use ryo_mutations::basic::InlineTraitMutation;
let symbol_id = try_resolve!(
self,
target,
ctx,
index,
spec_type,
affected_symbols,
"Failed to resolve target symbol"
);
let mut mutation = InlineTraitMutation::new(symbol_id, struct_name.clone());
if !remove_trait {
mutation = mutation.keep_trait();
}
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ReplaceExpr {
fn_id,
old_expr,
new_expr,
replace_all,
..
} => {
use ryo_mutations::basic::stmt::ReplaceExprMutation;
use ryo_source::pure::{PureExpr, ToPure};
let target_fn = match fn_id {
Some(id) => *id,
None => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(
"ReplaceExpr requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let old_pure = syn::parse_str::<syn::Expr>(old_expr)
.map(|e| e.to_pure())
.unwrap_or_else(|_| PureExpr::Path(old_expr.clone()));
let new_pure = syn::parse_str::<syn::Expr>(new_expr)
.map(|e| e.to_pure())
.unwrap_or_else(|_| PureExpr::Path(new_expr.clone()));
let mut mutation = ReplaceExprMutation::new(old_pure, new_pure, target_fn);
if !replace_all {
mutation = mutation.first_only();
}
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::RemoveStatement {
fn_id,
ref pattern,
remove_all,
..
} => {
use crate::executor::registry::converters::StmtConverter;
use ryo_mutations::basic::stmt::RemoveStatementMutation;
let target_fn = match fn_id {
Some(id) => *id,
None => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(
"RemoveStatement requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let target_stmt = match StmtConverter::parse_stmt(pattern) {
Ok(s) => s,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(format!("Failed to parse statement pattern: {}", e)),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let mut mutation =
RemoveStatementMutation::new(target_stmt, pattern.clone(), target_fn);
if !*remove_all {
mutation = mutation.first_only();
}
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::InsertStatement {
fn_id,
ref stmt,
ref position,
ref reference_pattern,
..
} => {
use crate::executor::registry::converters::StmtConverter;
use ryo_mutations::basic::stmt::InsertStatementMutation;
let pure_stmt = match StmtConverter::parse_stmt(stmt) {
Ok(s) => s,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(format!("Failed to parse statement: {}", e)),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let mut mutation = InsertStatementMutation::new(pure_stmt, *fn_id);
mutation = match position {
StmtInsertPosition::Start => mutation.at_start(),
StmtInsertPosition::End => mutation.at_end(),
StmtInsertPosition::BeforePattern => {
if let Some(ref p) = reference_pattern {
let reference_stmt = match StmtConverter::parse_stmt(p) {
Ok(s) => s,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(format!(
"Failed to parse reference pattern: {}",
e
)),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
mutation.before(reference_stmt)
} else {
mutation
}
}
StmtInsertPosition::AfterPattern => {
if let Some(ref p) = reference_pattern {
let reference_stmt = match StmtConverter::parse_stmt(p) {
Ok(s) => s,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(format!(
"Failed to parse reference pattern: {}",
e
)),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
mutation.after(reference_stmt)
} else {
mutation
}
}
};
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::ReplaceStatement {
old_stmt,
new_stmt,
fn_id,
..
} => {
use crate::executor::registry::converters::StmtConverter;
use ryo_mutations::basic::stmt::ReplaceStatementMutation;
let old_pure = match StmtConverter::parse_stmt(old_stmt) {
Ok(s) => s,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(format!("Failed to parse old statement: {}", e)),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let new_pure = match StmtConverter::parse_stmt(new_stmt) {
Ok(s) => s,
Err(e) => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(format!("Failed to parse new statement: {}", e)),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let target_fn = match fn_id {
Some(id) => *id,
None => {
return SpecResult {
index,
spec_type,
changes: 0,
affected_files: vec![],
affected_symbols: vec![],
success: false,
error: Some(
"ReplaceStatement requires fn_id in V1 executor. Use V2 converter for all-functions support.".to_string()
),
registry_updates: RegistryUpdateBatch::default(),
events: vec![],
};
}
};
let mutation = ReplaceStatementMutation::new(old_pure, new_pure, target_fn);
ASTMutationEngine::execute_ast_reg(&mutation, ctx)
}
MutationSpec::DuplicateFunction { .. }
| MutationSpec::DuplicateStruct { .. }
| MutationSpec::DuplicateEnum { .. }
| MutationSpec::DuplicateModTree { .. } => {
return SpecResult {
index,
spec_type,
success: false,
changes: 0,
affected_files: vec![],
affected_symbols,
error: Some(
"Duplicate mutations must be routed through \
DuplicateConverter::convert_v2(); the V1 executor path \
has been removed."
.to_string(),
),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
MutationSpec::AddSpec { .. } => {
ExecutionResult::new(
MutationResult {
mutation_type: "AddSpec".to_string(),
changes: 0,
description: "V2 pending - implement as AddTypeAlias composition"
.to_string(),
},
vec![],
)
}
MutationSpec::PluginTransform { .. } => {
ExecutionResult::new(
MutationResult {
mutation_type: "PluginTransform".to_string(),
changes: 0,
description: "WASM plugin runtime not implemented in V2".to_string(),
},
vec![],
)
}
_ => {
return SpecResult {
index,
spec_type: spec_type.clone(),
success: false,
changes: 0,
affected_files: vec![],
affected_symbols,
error: Some(format!(
"MutationSpec::{} is not implemented in the V2 AST path \
(no converter or fallback covers this variant). \
If you need this spec, add a converter to \
MutationRegistry::convert_v2 or extend execute_spec_v2.",
spec_type
)),
registry_updates: RegistryUpdateBatch::new(),
events: vec![],
};
}
};
SpecResult {
index,
spec_type,
changes: exec_result.result.changes,
affected_files: vec![], affected_symbols,
success: exec_result.has_changes() || exec_result.result.changes == 0,
error: None,
registry_updates: RegistryUpdateBatch::new(),
events: exec_result.events,
}
}
pub fn with_strategy(mut self, strategy: ExecutionStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_verify(mut self, verify: bool) -> Self {
self.verify_after_each = verify;
self
}
pub fn with_stop_on_error(mut self, stop: bool) -> Self {
self.stop_on_error = stop;
self
}
}
fn normalize_generic_name(name: &str) -> String {
name.replace(" < ", "<")
.replace(" > ", ">")
.replace("< ", "<")
.replace(" >", ">")
.replace(", ", ",")
.replace(" ,", ",")
}
fn register_item_from_content(
ctx: &mut AnalysisContext,
target: &ryo_symbol::SymbolPath,
content: &str,
) {
use ryo_symbol::SymbolKind;
let trimmed = content.trim();
let mut lines = trimmed.lines();
let mut decl_line = "";
for line in lines.by_ref() {
let line = line.trim();
if !line.starts_with('#') && !line.starts_with("//") && !line.is_empty() {
decl_line = line;
break;
}
}
let tokens: Vec<&str> = decl_line.split_whitespace().collect();
if tokens.is_empty() {
return;
}
let mut idx = 0;
if tokens.get(idx) == Some(&"pub") {
idx += 1;
if let Some(t) = tokens.get(idx) {
if t.starts_with('(') {
idx += 1;
}
}
}
let Some(keyword) = tokens.get(idx) else {
return;
};
idx += 1;
let kind = match *keyword {
"struct" => SymbolKind::Struct,
"enum" => SymbolKind::Enum,
"fn" => SymbolKind::Function,
"type" => SymbolKind::TypeAlias,
"const" => SymbolKind::Const,
"static" => SymbolKind::Static,
"trait" => SymbolKind::Trait,
"mod" => SymbolKind::Mod,
"impl" => return, _ => return,
};
let Some(name_token) = tokens.get(idx) else {
return;
};
let name = name_token
.split(['<', '{', '(', ':'])
.next()
.unwrap_or(name_token);
let target_str = target.to_string();
let is_crate_root = target_str == "crate";
let full_path = if is_crate_root {
ryo_symbol::SymbolPath::parse(&format!("crate::{}", name))
} else {
ryo_symbol::SymbolPath::parse(&format!("{}::{}", target_str, name))
};
let Ok(path) = full_path else {
return;
};
let _ = ctx.registry_mut().register(path, kind);
}
fn spec_type_name(spec: &MutationSpec) -> String {
match spec {
MutationSpec::Rename { .. } => "Rename".to_string(),
MutationSpec::AddField { .. } => "AddField".to_string(),
MutationSpec::RemoveField { .. } => "RemoveField".to_string(),
MutationSpec::ChangeVisibility { .. } => "ChangeVisibility".to_string(),
MutationSpec::AddDerive { .. } => "AddDerive".to_string(),
MutationSpec::RemoveDerive { .. } => "RemoveDerive".to_string(),
MutationSpec::AddVariant { .. } => "AddVariant".to_string(),
MutationSpec::RemoveVariant { .. } => "RemoveVariant".to_string(),
MutationSpec::AddMatchArm { .. } => "AddMatchArm".to_string(),
MutationSpec::RemoveMatchArm { .. } => "RemoveMatchArm".to_string(),
MutationSpec::ReplaceMatchArm { .. } => "ReplaceMatchArm".to_string(),
MutationSpec::AddStructLiteralField { .. } => "AddStructLiteralField".to_string(),
MutationSpec::RemoveStructLiteralField { .. } => "RemoveStructLiteralField".to_string(),
MutationSpec::AddItem { .. } => "AddItem".to_string(),
MutationSpec::RemoveItem { .. } => "RemoveItem".to_string(),
MutationSpec::AddMethod { .. } => "AddMethod".to_string(),
MutationSpec::RemoveMethod { .. } => "RemoveMethod".to_string(),
MutationSpec::RemoveMod { .. } => "RemoveMod".to_string(),
MutationSpec::CreateMod { .. } => "CreateMod".to_string(),
MutationSpec::OrganizeImports { .. } => "OrganizeImports".to_string(),
MutationSpec::LoopToIterator { .. } => "LoopToIterator".to_string(),
MutationSpec::UnwrapToQuestion { .. } => "UnwrapToQuestion".to_string(),
MutationSpec::AddSpec { .. } => "AddSpec".to_string(),
MutationSpec::RemoveSpec { .. } => "RemoveSpec".to_string(),
MutationSpec::ValidateSpec { .. } => "ValidateSpec".to_string(),
MutationSpec::ExtractTrait { .. } => "ExtractTrait".to_string(),
MutationSpec::InlineTrait { .. } => "InlineTrait".to_string(),
MutationSpec::ReplaceType { .. } => "ReplaceType".to_string(),
MutationSpec::EnumToTrait { .. } => "EnumToTrait".to_string(),
MutationSpec::MoveItem { .. } => "MoveItem".to_string(),
MutationSpec::AssignOp { .. } => "AssignOp".to_string(),
MutationSpec::BoolSimplify { .. } => "BoolSimplify".to_string(),
MutationSpec::CloneOnCopy { .. } => "CloneOnCopy".to_string(),
MutationSpec::CollapsibleIf { .. } => "CollapsibleIf".to_string(),
MutationSpec::NoOpArmToTodo { .. } => "NoOpArmToTodo".to_string(),
MutationSpec::ComparisonToMethod { .. } => "ComparisonToMethod".to_string(),
MutationSpec::RedundantClosure { .. } => "RedundantClosure".to_string(),
MutationSpec::IntroduceVariable { .. } => "IntroduceVariable".to_string(),
MutationSpec::ManualMap { .. } => "ManualMap".to_string(),
MutationSpec::MatchToIfLet { .. } => "MatchToIfLet".to_string(),
MutationSpec::FilterNext { .. } => "FilterNext".to_string(),
MutationSpec::MapUnwrapOr { .. } => "MapUnwrapOr".to_string(),
MutationSpec::ReplaceExpr { .. } => "ReplaceExpr".to_string(),
MutationSpec::RemoveStatement { .. } => "RemoveStatement".to_string(),
MutationSpec::InsertStatement { .. } => "InsertStatement".to_string(),
MutationSpec::ReplaceStatement { .. } => "ReplaceStatement".to_string(),
MutationSpec::PluginTransform { .. } => "PluginTransform".to_string(),
MutationSpec::DuplicateFunction { .. } => "DuplicateFunction".to_string(),
MutationSpec::DuplicateStruct { .. } => "DuplicateStruct".to_string(),
MutationSpec::DuplicateEnum { .. } => "DuplicateEnum".to_string(),
MutationSpec::DuplicateModTree { .. } => "DuplicateModTree".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::executor::spec::{Scope, SelfParam, SymbolPath, Visibility};
use ryo_analysis::testing::{ContextBuilder, ContextTestExt};
use ryo_symbol::SymbolId;
fn dummy_id(index: u32) -> SymbolId {
SymbolId::parse(&format!("{}v1", index)).expect("valid dummy id")
}
fn execute_and_sync(
executor: &BlueprintExecutor,
blueprint: &ParallelBlueprint,
ctx: &mut AnalysisContext,
) -> BlueprintResult {
let result = executor.execute_v2(blueprint, ctx);
if result.success {
BlueprintExecutor::sync_files_and_rebuild(&result, ctx).unwrap();
}
result
}
fn create_test_context() -> AnalysisContext {
let code = r#"
struct Config {
name: String,
}
impl Config {
fn new() -> Self {
Self { name: String::new() }
}
}
"#;
ContextBuilder::new()
.with_file("src/config.rs", code)
.build()
}
#[test]
fn test_blueprint_executor_rename() {
let mut ctx = create_test_context();
let symbol_id = ctx
.registry()
.lookup_by_name("Config")
.expect("Config should exist in registry");
let specs = vec![MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "AppConfig".to_string(),
scope: Scope::Project,
}];
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(exec_result.success);
assert!(exec_result.total_changes > 0);
let file = ctx.test_file("src/config.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("AppConfig"));
assert!(!source.contains("struct Config"));
}
#[test]
fn test_blueprint_executor_add_derive() {
let mut ctx = create_test_context();
let path = SymbolPath::parse("test_crate::config::Config").unwrap();
let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
let specs = vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(symbol_id),
derives: vec!["Debug".to_string(), "Clone".to_string()],
}];
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(exec_result.success);
let file = ctx.test_file("src/config.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("derive"), "Expected derive in: {}", source);
assert!(source.contains("Debug"));
}
#[test]
fn test_blueprint_executor_organize_imports() {
let code = r#"
use std::collections::HashMap;
use std::io::Write;
use std::collections::HashSet;
use std::io::Read;
fn main() {}
"#;
let mut ctx = ContextBuilder::new().with_file("src/main.rs", code).build();
let specs = vec![MutationSpec::OrganizeImports {
module_id: None,
deduplicate: true,
merge_groups: true,
}];
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(exec_result.success);
}
#[test]
fn test_blueprint_with_conflicts_fails_when_not_ignored() {
use ryo_symbol::{SymbolKind, SymbolPath, SymbolRegistry};
let mut ctx = create_test_context();
let mut symbol_registry = SymbolRegistry::new();
let path_a = SymbolPath::parse("test_crate::A").unwrap();
let symbol_a = symbol_registry
.register(path_a, SymbolKind::Struct)
.unwrap();
let specs = vec![
MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_a),
to: "B".to_string(),
scope: Scope::Project,
},
MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_a),
to: "C".to_string(),
scope: Scope::Project,
},
];
let blueprint = ParallelBlueprint::from_mutations(specs);
assert!(
!blueprint.conflicts.is_empty(),
"Blueprint should have conflicts"
);
let mut executor = BlueprintExecutor::new();
executor.ignore_conflicts = false;
let result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
!result.success,
"Execution should fail when conflicts are not ignored"
);
assert!(result.error.is_some(), "Should have error message");
assert!(
result.error.unwrap().contains("conflict"),
"Error should mention conflicts"
);
}
#[test]
fn test_blueprint_executor_add_item_struct() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// empty file\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "pub struct Config {}".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem struct failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("pub struct Config"),
"Struct not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_item_fn() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// empty file\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "fn helper() {}".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem fn failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("fn helper"),
"Function not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_item_use() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Dummy;\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "use HashMap;".to_string(),
position: super::super::spec::InsertPosition::Top,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem use failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("use HashMap"), "Use not added: {}", source);
}
#[test]
fn test_blueprint_executor_add_item_impl() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "struct Foo {}\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "impl Foo {}".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem impl failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("impl Foo"), "Impl not added: {}", source);
}
#[test]
fn test_blueprint_executor_add_item_enum() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// empty file\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "pub enum Status { Pending, Active, Completed }".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem enum failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("pub enum Status"),
"Enum not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_method_basic() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Config {}\n\nimpl Config {}\n")
.build();
let spec = MutationSpec::AddMethod {
target: MutationTargetSymbol::ByKindAndName(
crate::executor::ItemKind::Impl,
"Config".to_string(),
),
method_name: "new".to_string(),
params: vec![],
return_type: Some("Self".to_string()),
body: "Self {}".to_string(),
is_pub: true,
self_param: None,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddMethod basic failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("pub fn new"),
"Method not added: {}",
source
);
assert!(
source.contains("-> Self"),
"Return type not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_method_with_self() {
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
"pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
)
.build();
let spec = MutationSpec::AddMethod {
target: MutationTargetSymbol::ByKindAndName(
crate::executor::ItemKind::Impl,
"Counter".to_string(),
),
method_name: "get".to_string(),
params: vec![],
return_type: Some("u32".to_string()),
body: "self.value".to_string(),
is_pub: true,
self_param: Some(SelfParam::Ref),
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddMethod with_self failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("&self"), "Self param not added: {}", source);
assert!(source.contains("fn get"), "Method not added: {}", source);
}
#[test]
fn test_blueprint_executor_add_method_with_mut_self() {
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
"pub struct Counter { value: u32 }\n\nimpl Counter {}\n",
)
.build();
let spec = MutationSpec::AddMethod {
target: MutationTargetSymbol::ByKindAndName(
crate::executor::ItemKind::Impl,
"Counter".to_string(),
),
method_name: "increment".to_string(),
params: vec![],
return_type: None,
body: "self.value += 1".to_string(),
is_pub: true,
self_param: Some(SelfParam::Mut),
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddMethod with_mut_self failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("&mut self"),
"Mut self param not added: {}",
source
);
assert!(
source.contains("fn increment"),
"Method not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_method_with_params() {
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
"pub struct Calculator {}\n\nimpl Calculator {}\n",
)
.build();
let spec = MutationSpec::AddMethod {
target: MutationTargetSymbol::ByKindAndName(
crate::executor::ItemKind::Impl,
"Calculator".to_string(),
),
method_name: "add".to_string(),
params: vec![
("a".to_string(), "i32".to_string()),
("b".to_string(), "i32".to_string()),
],
return_type: Some("i32".to_string()),
body: "a + b".to_string(),
is_pub: true,
self_param: None,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddMethod with_params failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("fn add"), "Method not added: {}", source);
assert!(source.contains("a: i32"), "Param a not added: {}", source);
assert!(source.contains("b: i32"), "Param b not added: {}", source);
}
#[test]
fn test_blueprint_executor_multi_file_rename() {
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
r#"mod models;
fn process(task: Task) -> Task {
task
}
"#,
)
.with_file(
"src/models.rs",
r#"pub struct Task {
pub id: u32,
pub name: String,
}
"#,
)
.build();
let symbol_id = ctx
.registry()
.lookup_by_name("Task")
.expect("Task should exist in registry");
let spec = MutationSpec::Rename {
target: MutationTargetSymbol::ById(symbol_id),
to: "TodoItem".to_string(),
scope: Scope::Project,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"Multi-file rename failed: {:?}",
exec_result.error
);
let lib = ctx.test_file("src/lib.rs").unwrap();
let lib_source = lib.to_source().unwrap();
assert!(
lib_source.contains("task: TodoItem"),
"lib.rs type not renamed: {}",
lib_source
);
assert!(
lib_source.contains("-> TodoItem"),
"lib.rs return type not renamed: {}",
lib_source
);
let models = ctx.test_file("src/models.rs").unwrap();
let models_source = models.to_source().unwrap();
assert!(
models_source.contains("struct TodoItem"),
"models.rs struct not renamed: {}",
models_source
);
assert!(
!models_source.contains("struct Task"),
"models.rs still has struct Task: {}",
models_source
);
}
#[test]
fn test_blueprint_executor_multi_file_add_items() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub mod models;\n")
.with_file("src/models.rs", "pub struct Placeholder;\n")
.build();
let spec1 = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::models").unwrap(),
)),
content: "pub struct User { id: u32 }".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let spec2 = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "use models::User;".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"Multi-file add items failed: {:?}",
exec_result.error
);
let models = ctx.test_file("src/models.rs").unwrap();
let models_source = models.to_source().unwrap();
assert!(
models_source.contains("pub struct User"),
"User not added to models.rs: {}",
models_source
);
let lib = ctx.test_file("src/lib.rs").unwrap();
let lib_source = lib.to_source().unwrap();
assert!(
lib_source.contains("use models::User"),
"Use not added to lib.rs: {}",
lib_source
);
}
#[test]
fn test_blueprint_executor_add_item_generic_struct() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// empty file\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "pub struct Container<T> { value: T }".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem generic struct failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("struct Container"),
"Generic struct not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_item_async_fn() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// empty file\n")
.build();
let spec = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "pub async fn fetch_data() -> String { String::new() }".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddItem async fn failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("fn fetch_data"),
"Async fn not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_field_to_generic_struct() {
use ryo_analysis::SymbolKind;
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "pub struct Wrapper<T> { inner: T }\n")
.build();
let symbol_id = ctx
.registry
.iter()
.find(|(id, path)| {
path.name() == "Wrapper" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
})
.map(|(id, _)| id)
.expect("Wrapper struct not found in registry");
let spec = MutationSpec::AddField {
target: MutationTargetSymbol::ById(symbol_id),
field_name: "count".to_string(),
field_type: "usize".to_string(),
visibility: Visibility::Pub,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddField to generic struct failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("pub count: usize"),
"Field not added to generic struct: {}",
source
);
}
#[test]
fn test_blueprint_executor_add_derive_to_generic_struct() {
let mut ctx = ContextBuilder::new()
.with_file(
"src/lib.rs",
"pub struct Pair<T, U> { first: T, second: U }\n",
)
.build();
let path = SymbolPath::parse("test_crate::Pair").unwrap();
let symbol_id = ctx.registry().lookup(&path).expect("Pair should exist");
let spec = MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(symbol_id),
derives: vec!["Debug".to_string(), "Clone".to_string()],
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"AddDerive to generic struct failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("Debug"),
"Debug derive not added: {}",
source
);
assert!(
source.contains("Clone"),
"Clone derive not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_create_mod_declaration() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "use std::io;\n\nfn main() {}\n")
.build();
let spec = MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
mod_name: "models".to_string(),
content: String::new(),
is_pub: false,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"CreateMod failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("mod models;"), "Mod not added: {}", source);
}
#[test]
fn test_blueprint_executor_create_pub_mod_declaration() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "fn main() {}\n")
.build();
let spec = MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
mod_name: "api".to_string(),
content: String::new(),
is_pub: true,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"CreateMod pub failed: {:?}",
exec_result.error
);
let file = ctx.test_file("src/lib.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("pub mod api;"),
"Pub mod not added: {}",
source
);
}
#[test]
fn test_blueprint_executor_create_file() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib.rs\n")
.build();
let spec = MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
mod_name: "models".to_string(),
content: "pub struct Model { id: u32 }".to_string(),
is_pub: true,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"CreateFile failed: {:?}",
exec_result.error
);
assert!(ctx.test_file("src/models.rs").is_some(), "File not created");
let file = ctx.test_file("src/models.rs").unwrap();
let source = file.to_source().unwrap();
assert!(
source.contains("struct Model"),
"Content not correct: {}",
source
);
}
#[test]
fn test_blueprint_executor_create_module_workflow() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "fn main() {}\n")
.build();
let spec = MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
mod_name: "utils".to_string(),
content: "pub fn helper() {}".to_string(),
is_pub: true,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new();
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"Module workflow failed: {:?}",
exec_result.error
);
let lib = ctx.test_file("src/lib.rs").unwrap();
let lib_source = lib.to_source().unwrap();
assert!(
lib_source.contains("pub mod utils;"),
"Mod not added to lib: {}",
lib_source
);
let utils = ctx.test_file("src/utils.rs").unwrap();
let utils_source = utils.to_source().unwrap();
assert!(
utils_source.contains("fn helper"),
"Function not in utils: {}",
utils_source
);
}
#[test]
fn test_wavefront_execution_basic() {
let mut ctx = create_test_context();
let path = SymbolPath::parse("test_crate::config::Config").unwrap();
let symbol_id = ctx.registry().lookup(&path).expect("Config should exist");
let specs = vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(symbol_id),
derives: vec!["Debug".to_string()],
}];
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"Wavefront basic failed: {:?}",
exec_result.error
);
assert!(exec_result.total_changes > 0);
let file = ctx.test_file("src/config.rs").unwrap();
let source = file.to_source().unwrap();
assert!(source.contains("Debug"), "Derive not added: {}", source);
}
#[test]
fn test_wavefront_execution_multi_file() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "// lib\n")
.with_file("src/models.rs", "// models\n")
.build();
let spec1 = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate::models").unwrap(),
)),
content: "pub struct User { id: u32 }".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let spec2 = MutationSpec::AddItem {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
content: "fn main() {}".to_string(),
position: super::super::spec::InsertPosition::Bottom,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec1, spec2]);
let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"Wavefront multi-file failed: {:?}",
exec_result.error
);
let models = ctx.test_file("src/models.rs").unwrap();
assert!(
models.to_source().unwrap().contains("pub struct User"),
"User not added to models.rs"
);
let lib = ctx.test_file("src/lib.rs").unwrap();
assert!(
lib.to_source().unwrap().contains("fn main"),
"main not added to lib.rs"
);
}
#[test]
fn test_wavefront_execution_with_dependencies() {
let mut ctx = ContextBuilder::new()
.with_file("src/lib.rs", "fn main() {}\n")
.build();
let spec = MutationSpec::CreateMod {
target: MutationTargetSymbol::ByPath(Box::new(
SymbolPath::parse("test_crate").unwrap(),
)),
mod_name: "api".to_string(),
content: "pub fn endpoint() {}".to_string(),
is_pub: true,
};
let blueprint = ParallelBlueprint::from_mutations(vec![spec]);
let executor = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
let exec_result = execute_and_sync(&executor, &blueprint, &mut ctx);
assert!(
exec_result.success,
"Wavefront with deps failed: {:?}",
exec_result.error
);
let lib = ctx.test_file("src/lib.rs").unwrap();
assert!(
lib.to_source().unwrap().contains("pub mod api;"),
"Mod not added to lib"
);
let api = ctx.test_file("src/api.rs").unwrap();
assert!(
api.to_source().unwrap().contains("fn endpoint"),
"Function not in api"
);
}
#[test]
fn test_suggest_strategy() {
let specs = vec![MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".to_string()],
}];
let blueprint = ParallelBlueprint::from_mutations(specs);
assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
let specs = vec![
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".to_string()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Clone".to_string()],
},
];
let blueprint = ParallelBlueprint::from_mutations(specs);
assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Sequential);
let specs = vec![
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".to_string()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Clone".to_string()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Default".to_string()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Hash".to_string()],
},
];
let blueprint = ParallelBlueprint::from_mutations(specs);
assert_eq!(suggest_strategy(&blueprint), ExecutionStrategy::Wavefront);
}
#[test]
#[ignore = "V1 path disabled - needs V2 migration"]
fn test_sequential_and_wavefront_produce_same_result() {
let code = r#"
struct A {}
struct B {}
struct C {}
"#;
let specs = vec![
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Debug".to_string()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Clone".to_string()],
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(dummy_id(1)),
derives: vec!["Default".to_string()],
},
];
let blueprint = ParallelBlueprint::from_mutations(specs.clone());
let mut ctx_seq = ContextBuilder::new().with_file("src/lib.rs", code).build();
let executor_seq = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Sequential);
let result_seq = execute_and_sync(&executor_seq, &blueprint, &mut ctx_seq);
let mut ctx_wave = ContextBuilder::new().with_file("src/lib.rs", code).build();
let executor_wave = BlueprintExecutor::new().with_strategy(ExecutionStrategy::Wavefront);
let result_wave = execute_and_sync(&executor_wave, &blueprint, &mut ctx_wave);
assert!(result_seq.success, "Sequential failed");
assert!(result_wave.success, "Wavefront failed");
let source_seq = ctx_seq
.test_file("src/lib.rs")
.unwrap()
.to_source()
.unwrap();
let source_wave = ctx_wave
.test_file("src/lib.rs")
.unwrap()
.to_source()
.unwrap();
assert!(source_seq.contains("Debug"), "Sequential missing Debug");
assert!(source_seq.contains("Clone"), "Sequential missing Clone");
assert!(source_seq.contains("Default"), "Sequential missing Default");
assert!(source_wave.contains("Debug"), "Wavefront missing Debug");
assert!(source_wave.contains("Clone"), "Wavefront missing Clone");
assert!(source_wave.contains("Default"), "Wavefront missing Default");
}
#[test]
#[ignore = "flaky: µs-level timing comparison depends on CPU load"]
fn test_execute_v2_without_sync_is_faster() {
use ryo_analysis::SymbolKind;
use std::time::Instant;
let code = r#"
pub struct Config { name: String, value: i32 }
pub struct User { id: u64, name: String, email: String }
pub struct Order { id: u64, user_id: u64, total: f64 }
pub enum Status { Pending, Active, Completed, Failed }
pub trait Processor { fn process(&self); }
impl Processor for Config { fn process(&self) {} }
impl Processor for User { fn process(&self) {} }
"#;
fn create_specs(ctx: &ryo_analysis::AnalysisContext) -> Vec<MutationSpec> {
let config_id = ctx
.registry
.iter()
.find(|(id, path)| {
path.name() == "Config" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
})
.map(|(id, _)| id)
.expect("Config not found");
let user_id = ctx
.registry
.iter()
.find(|(id, path)| {
path.name() == "User" && ctx.registry.kind(*id) == Some(SymbolKind::Struct)
})
.map(|(id, _)| id)
.expect("User not found");
vec![
MutationSpec::AddField {
target: MutationTargetSymbol::ById(config_id),
field_name: "enabled".to_string(),
field_type: "bool".to_string(),
visibility: Visibility::Pub,
},
MutationSpec::AddDerive {
target: MutationTargetSymbol::ById(user_id),
derives: vec!["Debug".to_string(), "Clone".to_string()],
},
]
}
let iterations = 10;
let mut execute_only_times = Vec::with_capacity(iterations);
for _ in 0..iterations {
let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
let specs = create_specs(&ctx);
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new();
let t0 = Instant::now();
let result = executor.execute_v2(&blueprint, &mut ctx);
execute_only_times.push(t0.elapsed());
assert!(result.success);
}
let mut execute_with_sync_times = Vec::with_capacity(iterations);
for _ in 0..iterations {
let mut ctx = ContextBuilder::new().with_file("src/lib.rs", code).build();
let specs = create_specs(&ctx);
let blueprint = ParallelBlueprint::from_mutations(specs);
let executor = BlueprintExecutor::new();
let t0 = Instant::now();
let result = executor.execute_v2(&blueprint, &mut ctx);
BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).unwrap();
execute_with_sync_times.push(t0.elapsed());
assert!(result.success);
}
let avg_execute_only: u128 = execute_only_times
.iter()
.map(|d| d.as_micros())
.sum::<u128>()
/ iterations as u128;
let avg_with_sync: u128 = execute_with_sync_times
.iter()
.map(|d| d.as_micros())
.sum::<u128>()
/ iterations as u128;
eprintln!(
"execute_v2 only: {}µs avg, execute_v2 + sync: {}µs avg, sync overhead: {:.1}x",
avg_execute_only,
avg_with_sync,
avg_with_sync as f64 / avg_execute_only as f64
);
assert!(
avg_execute_only < avg_with_sync,
"execute_v2 only ({}µs) should be faster than with sync ({}µs)",
avg_execute_only,
avg_with_sync
);
}
}