diff --git a/src/build.rs b/src/build.rs index 59173b6..a8d98ed 100644 --- a/src/build.rs +++ b/src/build.rs @@ -19,7 +19,7 @@ struct BuildInner { #[derive(Debug)] enum BuildMode { - Cargo, + Cargo { args: Option> }, Script(PathBuf), Rustc, } @@ -28,10 +28,15 @@ impl Build { pub fn new(options: &Options) -> Self { let mode = if options.rustc { BuildMode::Rustc - } else if let Some(script) = &options.verify_error_path { + } else if let Some(script) = &options.script_path { BuildMode::Script(script.clone()) } else { - BuildMode::Cargo + BuildMode::Cargo { + args: options + .cargo_args + .as_ref() + .map(|cmd| cmd.split_whitespace().map(ToString::to_string).collect()), + } }; Self { inner: Rc::new(BuildInner { @@ -51,10 +56,14 @@ impl Build { } let reproduces_issue = match &self.inner.mode { - BuildMode::Cargo => { + BuildMode::Cargo { args } => { let mut cmd = Command::new("cargo"); cmd.arg("build"); + for arg in args.into_iter().flatten() { + cmd.arg(arg); + } + let output = String::from_utf8(cmd.output().context("spawning rustc process")?.stderr) .unwrap(); @@ -86,11 +95,15 @@ impl Build { } pub fn get_diags(&self) -> Result<(Vec, Vec)> { - let diags = match self.inner.mode { - BuildMode::Cargo => { + let diags = match &self.inner.mode { + BuildMode::Cargo { args } => { let mut cmd = Command::new("cargo"); cmd.args(["build", "--message-format=json"]); + for arg in args.into_iter().flatten() { + cmd.arg(arg); + } + let cmd_output = cmd.output()?; let output = String::from_utf8(cmd_output.stdout.clone())?; diff --git a/src/everybody_loops.rs b/src/everybody_loops.rs index 4bbd129..d8cbc08 100644 --- a/src/everybody_loops.rs +++ b/src/everybody_loops.rs @@ -27,11 +27,12 @@ impl VisitMut for Visitor<'_> { match block.stmts.as_slice() { [syn::Stmt::Expr(syn::Expr::Loop(syn::ExprLoop { body: loop_body, .. - }))] if loop_body.stmts.is_empty() && self.checker.can_process(&self.current_path) => {} - _ => { + }))] if loop_body.stmts.is_empty() => {} + _ if self.checker.can_process(&self.current_path) => { *block = self.loop_expr.clone(); self.process_state = ProcessState::Changed; } + _ => {} } } diff --git a/src/lib.rs b/src/lib.rs index 2f18bf6..29a21c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,11 @@ enum Cargo { #[derive(clap::Args, Debug)] pub struct Options { #[arg(short, long)] - verify_error_path: Option, + script_path: Option, + + #[arg(long)] + cargo_args: Option, + #[arg(long)] rustc: bool, #[arg(long)] @@ -38,8 +42,6 @@ pub fn minimize() -> Result<()> { let mut minimizer = Minimizer::new_glob_dir(&options.path, build); - minimizer.delete_dead_code().context("deleting dead code")?; - minimizer.run_passes([ Box::new(Privatize::default()) as Box, Box::new(EverybodyLoops::default()) as Box, diff --git a/src/processor/mod.rs b/src/processor/mod.rs index 43b961e..c9cea22 100644 --- a/src/processor/mod.rs +++ b/src/processor/mod.rs @@ -1,7 +1,7 @@ mod files; mod reaper; -use std::{collections::HashSet, ffi::OsStr, path::Path}; +use std::{borrow::Borrow, collections::HashSet, ffi::OsStr, mem, path::Path}; use anyhow::{ensure, Context, Result}; @@ -66,7 +66,7 @@ impl Minimizer { } pub fn run_passes<'a>( - &mut self, + &self, passes: impl IntoIterator>, ) -> Result<()> { let inital_build = self.build.build()?; @@ -83,7 +83,7 @@ impl Minimizer { Ok(()) } - fn run_pass(&mut self, pass: &mut dyn Processor) -> Result<()> { + fn run_pass(&self, pass: &mut dyn Processor) -> Result<()> { let mut invalidated_files = HashSet::new(); let mut refresh_and_try_again = false; @@ -97,39 +97,7 @@ impl Minimizer { continue; } - let file_display = file.path.display(); - - let mut change = file.try_change(&mut changes)?; - - let mut krate = syn::parse_file(change.before_content()) - .with_context(|| format!("parsing file {file_display}"))?; - - let has_made_change = pass.process_file(&mut krate, file, &mut PassController {}); - - match has_made_change { - ProcessState::Changed | ProcessState::FileInvalidated => { - let result = prettyplease::unparse(&krate); - - change.write(&result)?; - - let after = self.build.build()?; - - println!("{file_display}: After {}: {after}", pass.name()); - - if after.reproduces_issue() { - change.commit(); - } else { - change.rollback()?; - } - - if has_made_change == ProcessState::FileInvalidated { - invalidated_files.insert(file); - } - } - ProcessState::NoChange => { - println!("{file_display}: After {}: no change", pass.name()); - } - } + self.process_file(pass, file, &mut invalidated_files, &mut changes)?; } if !changes.had_changes() { @@ -149,14 +117,174 @@ impl Minimizer { } } } + + fn process_file<'file>( + &self, + pass: &mut dyn Processor, + file: &'file SourceFile, + invalidated_files: &mut HashSet<&'file SourceFile>, + changes: &mut Changes, + ) -> Result<()> { + let mut checker = PassController::new(); + + loop { + dbg!(&checker); + + let file_display = file.path.display(); + + let mut change = file.try_change(changes)?; + + let mut krate = syn::parse_file(change.before_content()) + .with_context(|| format!("parsing file {file_display}"))?; + + let has_made_change = pass.process_file(&mut krate, file, &mut checker); + + match has_made_change { + ProcessState::Changed | ProcessState::FileInvalidated => { + let result = prettyplease::unparse(&krate); + + change.write(&result)?; + + let after = self.build.build()?; + + println!("{file_display}: After {}: {after}", pass.name()); + + if after.reproduces_issue() { + change.commit(); + checker.reproduces(); + } else { + change.rollback()?; + checker.does_not_reproduce(); + } + + if has_made_change == ProcessState::FileInvalidated { + invalidated_files.insert(file); + } + } + ProcessState::NoChange => { + println!("{file_display}: After {}: no change", pass.name()); + checker.no_change(); + } + } + + if checker.is_finished() { + break; + } + } + + Ok(()) + } } -pub struct PassController {} +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct AstPath(Vec); + +impl Borrow<[String]> for AstPath { + fn borrow(&self) -> &[String] { + &self.0 + } +} + +#[derive(Debug)] +pub struct PassController { + state: PassControllerState, +} + +#[derive(Debug)] +enum PassControllerState { + InitialCollection { + candidates: Vec, + }, + + Bisecting { + current: HashSet, + worklist: Vec>, + }, + + Success, +} impl PassController { - pub fn can_process(&mut self, _: &[String]) -> bool { - // FIXME: Actually do smart things here. - true + fn new() -> Self { + Self { + state: PassControllerState::InitialCollection { + candidates: Vec::new(), + }, + } + } + + fn reproduces(&mut self) { + match &mut self.state { + PassControllerState::InitialCollection { .. } => { + self.state = PassControllerState::Success + } + PassControllerState::Bisecting { + current, worklist, .. + } => match worklist.pop() { + Some(next) => *current = next.into_iter().collect(), + None => { + self.state = PassControllerState::Success; + } + }, + PassControllerState::Success => unreachable!("Processed after success"), + } + } + + fn does_not_reproduce(&mut self) { + match &mut self.state { + PassControllerState::InitialCollection { candidates } => { + let candidates = mem::take(candidates); + let half = candidates.len() / 2; + let (first_half, second_half) = candidates.split_at(half); + + self.state = PassControllerState::Bisecting { + current: first_half.iter().cloned().collect(), + worklist: vec![second_half.to_owned()], + }; + } + PassControllerState::Bisecting { current, worklist } => { + dbg!(¤t, &worklist); + todo!(); + } + PassControllerState::Success => unreachable!("Processed after success"), + } + } + + fn no_change(&mut self) { + match &self.state { + PassControllerState::InitialCollection { candidates } => { + assert!( + candidates.is_empty(), + "No change but received candidates: {candidates:?}" + ); + self.state = PassControllerState::Success; + } + PassControllerState::Bisecting { current, .. } => { + unreachable!("No change while bisecting, current was empty somehow: {current:?}"); + } + PassControllerState::Success => {} + } + } + + fn is_finished(&mut self) -> bool { + match &mut self.state { + PassControllerState::InitialCollection { .. } => false, + PassControllerState::Bisecting { .. } => false, + PassControllerState::Success => true, + } + } + + pub fn can_process(&mut self, path: &[String]) -> bool { + match &mut self.state { + PassControllerState::InitialCollection { candidates } => { + candidates.push(AstPath(path.to_owned())); + true + } + PassControllerState::Bisecting { current, .. } => current.contains(path), + PassControllerState::Success => { + unreachable!("Processed further after success"); + } + } } } diff --git a/src/processor/reaper.rs b/src/processor/reaper.rs index 3b16ead..3b0f39b 100644 --- a/src/processor/reaper.rs +++ b/src/processor/reaper.rs @@ -226,16 +226,16 @@ impl<'a> FindUnusedFunction<'a> { .filter(|&matches| matches) .count(); - assert!( - span_matches < 2, - "multiple dead_code spans matched identifier: {span_matches}." - ); - - if span_matches == 1 { - self.process_state = ProcessState::FileInvalidated; + match span_matches { + 0 => true, + 1 => { + self.process_state = ProcessState::FileInvalidated; + !self.checker.can_process(&self.current_path) + } + _ => { + panic!("multiple dead_code spans matched identifier: {span_matches}."); + } } - - span_matches == 0 && self.checker.can_process(&self.current_path) } } @@ -246,9 +246,14 @@ impl VisitMut for FindUnusedFunction<'_> { item_impl.items.retain(|item| match item { ImplItem::Method(method) => { + self.current_path.push(method.sig.ident.to_string()); + let span = method.sig.ident.span(); - self.should_retain_item(span) + let should_retain = self.should_retain_item(span); + + self.current_path.pop(); + should_retain } _ => true, }); @@ -261,9 +266,13 @@ impl VisitMut for FindUnusedFunction<'_> { fn visit_file_mut(&mut self, krate: &mut syn::File) { krate.items.retain(|item| match item { Item::Fn(func) => { - let span = func.sig.ident.span(); + self.current_path.push(func.sig.ident.to_string()); - self.should_retain_item(span) + let span = func.sig.ident.span(); + let should_retain = self.should_retain_item(span); + + self.current_path.pop(); + should_retain } _ => true, }); @@ -277,9 +286,13 @@ impl VisitMut for FindUnusedFunction<'_> { if let Some((_, content)) = &mut module.content { content.retain(|item| match item { Item::Fn(func) => { - let span = func.sig.ident.span(); + self.current_path.push(func.sig.ident.to_string()); - self.should_retain_item(span) + let span = func.sig.ident.span(); + let should_retain = self.should_retain_item(span); + + self.current_path.pop(); + should_retain } _ => true, }) diff --git a/test-cases/unused-code/src/main.rs b/test-cases/unused-code/src/main.rs index 81027db..46fe8c7 100644 --- a/test-cases/unused-code/src/main.rs +++ b/test-cases/unused-code/src/main.rs @@ -1,8 +1,8 @@ fn unused() { - loop {} + this_is_required_to_error_haha(); } fn main() { - loop {} + other::unused(); } mod other; diff --git a/test-cases/unused-code/src/other.rs b/test-cases/unused-code/src/other.rs index 1dcbcc3..aefc744 100644 --- a/test-cases/unused-code/src/other.rs +++ b/test-cases/unused-code/src/other.rs @@ -1,3 +1,3 @@ -fn unused() { - loop {} +pub fn unused() { + }