make better

This commit is contained in:
nora 2022-12-20 17:18:53 +01:00
parent f7395cd817
commit 79b69fafb9
7 changed files with 182 additions and 82 deletions

View file

@ -92,7 +92,7 @@ impl Build {
let (is_ice, output) = match &self.inner.mode { let (is_ice, output) = match &self.inner.mode {
BuildMode::Cargo { args } => { BuildMode::Cargo { args } => {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.arg("build"); cmd.args(["build", "--color=always"]);
for arg in args.iter().flatten() { for arg in args.iter().flatten() {
cmd.arg(arg); cmd.arg(arg);
@ -107,8 +107,8 @@ impl Build {
let output = String::from_utf8(outputs.stderr)?; let output = String::from_utf8(outputs.stderr)?;
( (
outputs.status.code() == Some(101) // Cargo always exits with 101 when rustc has an error.
|| output.contains("internal compiler error"), output.contains("internal compiler error") || output.contains("' panicked at"),
output, output,
) )
} }

View file

@ -83,26 +83,5 @@ pub fn minimize() -> Result<()> {
minimizer.delete_dead_code().context("deleting dead code")?; minimizer.delete_dead_code().context("deleting dead code")?;
/*
let file = expand::expand(&dir).context("during expansion")?;
//let file = syn::parse_str("extern { pub fn printf(format: *const ::c_char, ...) -> ::c_int; }",).unwrap();
let file = prettyplease::unparse(&file);
println!("// EXPANDED-START\n\n{file}\n\n// EXPANDED-END");
std::fs::write("expanded.rs", file)?;
println!("wow, expanded");
*/
/*
let build = Build::new(cargo_dir);
if build.build()?.success {
bail!("build must initially fail!");
}
*/
Ok(()) Ok(())
} }

View file

@ -1,9 +1,14 @@
use anyhow::Result; use anyhow::Result;
use tracing::info; use tracing::{error, info, Level};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
fn main() -> Result<()> { fn main() -> Result<()> {
let registry = Registry::default().with(EnvFilter::from_default_env()); let registry = Registry::default().with(
EnvFilter::builder()
.with_default_directive(Level::INFO.into())
.from_env()
.unwrap(),
);
info!("Starting cargo-minimize"); info!("Starting cargo-minimize");
@ -13,7 +18,9 @@ fn main() -> Result<()> {
registry.with(tree_layer).init(); registry.with(tree_layer).init();
cargo_minimize::minimize()?; if let Err(err) = cargo_minimize::minimize() {
error!("An error occured:\n{err}");
}
Ok(()) Ok(())
} }

View file

@ -1,40 +1,50 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::{fs, path::{Path, PathBuf}}; use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, PartialEq, Eq, Clone, Hash)] #[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub(crate) struct SourceFile { pub(crate) struct SourceFile {
pub(crate) path: PathBuf, pub(crate) path: PathBuf,
} }
#[derive(Default)] #[derive(Default)]
pub(crate) struct Changes { pub(crate) struct Changes {
any_change: bool, any_change: bool,
} }
pub(crate) struct FileChange<'a, 'b> { pub(crate) struct FileChange<'a, 'b> {
pub(crate) path: &'a Path, pub(crate) path: &'a Path,
content: String, content: String,
changes: &'b mut Changes, changes: &'b mut Changes,
has_written_change: bool, has_written_change: bool,
} }
impl FileChange<'_, '_> { impl FileChange<'_, '_> {
pub(crate) fn before_content(&self) -> &str { pub(crate) fn before_content(&self) -> &str {
&self.content &self.content
} }
pub(crate) fn write(&mut self, new: &str) -> Result<()> { pub(crate) fn write(&mut self, new: &str) -> Result<()> {
self.has_written_change = true; self.has_written_change = true;
fs::write(self.path, new) fs::write(self.path, new).with_context(|| format!("writing file {}", self.path.display()))
.with_context(|| format!("writing file {}", self.path.display()))
} }
pub(crate) fn rollback(mut self) -> Result<()> { pub(crate) fn rollback(mut self) -> Result<()> {
assert!(self.has_written_change); assert!(self.has_written_change);
self.has_written_change = false; self.has_written_change = false;
fs::write(self.path, &self.content) fs::write(self.path, &self.content)
.with_context(|| format!("writing file {}", self.path.display())) .with_context(|| format!("writing file {}", self.path.display()))
} }
pub(crate) fn commit(mut self) { pub(crate) fn commit(mut self) {
assert!(self.has_written_change); assert!(self.has_written_change);
self.has_written_change = false; self.has_written_change = false;
self.changes.any_change = true; self.changes.any_change = true;
} }
} }
impl Drop for FileChange<'_, '_> { impl Drop for FileChange<'_, '_> {
fn drop(&mut self) { fn drop(&mut self) {
if self.has_written_change { if self.has_written_change {
@ -45,6 +55,7 @@ impl Drop for FileChange<'_, '_> {
} }
} }
} }
impl SourceFile { impl SourceFile {
pub(crate) fn try_change<'file, 'change>( pub(crate) fn try_change<'file, 'change>(
&'file self, &'file self,
@ -60,6 +71,7 @@ impl SourceFile {
}) })
} }
} }
impl Changes { impl Changes {
pub(crate) fn had_changes(&self) -> bool { pub(crate) fn had_changes(&self) -> bool {
self.any_change self.any_change

View file

@ -1,13 +1,22 @@
mod files; mod files;
mod reaper; mod reaper;
use std::{borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, mem, path::Path};
use anyhow::{Context, Result};
use crate::{build::Build, processor::files::Changes};
pub(crate) use self::files::SourceFile; pub(crate) use self::files::SourceFile;
use crate::{build::Build, processor::files::Changes};
use anyhow::{Context, Result};
use std::{
borrow::Borrow,
collections::{BTreeSet, HashSet},
ffi::OsStr,
fmt::Debug,
mem,
path::Path,
};
pub(crate) trait Processor { pub(crate) trait Processor {
fn refresh_state(&mut self) -> Result<()> { fn refresh_state(&mut self) -> Result<()> {
Ok(()) Ok(())
} }
/// Process a file. The state of the processor might get invalidated in the process as signaled with /// Process a file. The state of the processor might get invalidated in the process as signaled with
/// `ProcessState::FileInvalidated`. When a file is invalidated, the minimizer will call `Processor::refersh_state` /// `ProcessState::FileInvalidated`. When a file is invalidated, the minimizer will call `Processor::refersh_state`
/// before calling the this function on the same file again. /// before calling the this function on the same file again.
@ -17,24 +26,29 @@ pub(crate) trait Processor {
file: &SourceFile, file: &SourceFile,
checker: &mut PassController, checker: &mut PassController,
) -> ProcessState; ) -> ProcessState;
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
} }
impl Debug for dyn Processor { impl Debug for dyn Processor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name()) f.write_str(self.name())
} }
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) enum ProcessState { pub(crate) enum ProcessState {
NoChange, NoChange,
Changed, Changed,
FileInvalidated, FileInvalidated,
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Minimizer { pub(crate) struct Minimizer {
files: Vec<SourceFile>, files: Vec<SourceFile>,
build: Build, build: Build,
} }
impl Minimizer { impl Minimizer {
pub(crate) fn new_glob_dir(path: &Path, build: Build) -> Self { pub(crate) fn new_glob_dir(path: &Path, build: Build) -> Self {
let walk = walkdir::WalkDir::new(path); let walk = walkdir::WalkDir::new(path);
@ -43,7 +57,7 @@ impl Minimizer {
.filter_map(|entry| match entry { .filter_map(|entry| match entry {
Ok(entry) => Some(entry), Ok(entry) => Some(entry),
Err(err) => { Err(err) => {
eprintln!("WARN: Error in walkdir: {err}"); warn!("Error during walkdir: {err}");
None None
} }
}) })
@ -52,23 +66,28 @@ impl Minimizer {
path: entry.into_path(), path: entry.into_path(),
}) })
.inspect(|file| { .inspect(|file| {
println!("- {}", file.path.display()); info!("Collecting file: {}", file.path.display());
}) })
.collect(); .collect();
Self { files, build } Self { files, build }
} }
pub(crate) fn run_passes<'a>( pub(crate) fn run_passes<'a>(
&self, &self,
passes: impl IntoIterator<Item = Box<dyn Processor + 'a>>, passes: impl IntoIterator<Item = Box<dyn Processor + 'a>>,
) -> Result<()> { ) -> Result<()> {
let inital_build = self.build.build()?; let inital_build = self.build.build()?;
println!("Initial build: {inital_build}"); info!("Initial build: {inital_build}");
inital_build.require_reproduction("Initial")?; inital_build.require_reproduction("Initial")?;
for mut pass in passes { for mut pass in passes {
self.run_pass(&mut *pass)?; self.run_pass(&mut *pass)?;
} }
Ok(()) Ok(())
} }
fn run_pass(&self, pass: &mut dyn Processor) -> Result<()> { fn run_pass(&self, pass: &mut dyn Processor) -> Result<()> {
let mut invalidated_files = HashSet::new(); let mut invalidated_files = HashSet::new();
let mut refresh_and_try_again = false; let mut refresh_and_try_again = false;
@ -76,27 +95,32 @@ impl Minimizer {
let span = info_span!("Starting round of pass", name = pass.name()); let span = info_span!("Starting round of pass", name = pass.name());
let _enter = span.enter(); let _enter = span.enter();
let mut changes = Changes::default(); let mut changes = Changes::default();
for file in &self.files { for file in &self.files {
if invalidated_files.contains(file) { if invalidated_files.contains(file) {
continue; continue;
} }
self.process_file(pass, file, &mut invalidated_files, &mut changes)?; self.process_file(pass, file, &mut invalidated_files, &mut changes)?;
} }
if !changes.had_changes() { if !changes.had_changes() {
if !refresh_and_try_again && !invalidated_files.is_empty() { if !refresh_and_try_again && !invalidated_files.is_empty() {
pass.refresh_state().context("refreshing state for pass")?; pass.refresh_state().context("refreshing state for pass")?;
invalidated_files.clear(); invalidated_files.clear();
refresh_and_try_again = true; refresh_and_try_again = true;
println!("Refreshing files for {}", pass.name()); info!("Refreshing files for {}", pass.name());
continue; continue;
} }
println!("Finished {}", pass.name());
info!("Finished {}", pass.name());
return Ok(()); return Ok(());
} else { } else {
refresh_and_try_again = false; refresh_and_try_again = false;
} }
} }
} }
fn process_file<'file>( fn process_file<'file>(
&self, &self,
pass: &mut dyn Processor, pass: &mut dyn Processor,
@ -106,18 +130,19 @@ impl Minimizer {
) -> Result<()> { ) -> Result<()> {
let mut checker = PassController::new(); let mut checker = PassController::new();
loop { loop {
dbg!(& checker); debug!(?checker);
let file_display = file.path.display(); let file_display = file.path.display();
let mut change = file.try_change(changes)?; let mut change = file.try_change(changes)?;
let mut krate = syn::parse_file(change.before_content()) let mut krate = syn::parse_file(change.before_content())
.with_context(|| format!("parsing file {file_display}"))?; .with_context(|| format!("parsing file {file_display}"))?;
let has_made_change = pass.process_file(&mut krate, file, &mut checker); let has_made_change = pass.process_file(&mut krate, file, &mut checker);
match has_made_change { match has_made_change {
ProcessState::Changed | ProcessState::FileInvalidated => { ProcessState::Changed | ProcessState::FileInvalidated => {
let result = prettyplease::unparse(&krate); let result = prettyplease::unparse(&krate);
change.write(&result)?; change.write(&result)?;
let after = self.build.build()?; let after = self.build.build()?;
println!("{file_display}: After {}: {after}", pass.name()); info!("{file_display}: After {}: {after}", pass.name());
if after.reproduces_issue() { if after.reproduces_issue() {
change.commit(); change.commit();
checker.reproduces(); checker.reproduces();
@ -130,10 +155,11 @@ impl Minimizer {
} }
} }
ProcessState::NoChange => { ProcessState::NoChange => {
println!("{file_display}: After {}: no changes", pass.name()); info!("{file_display}: After {}: no changes", pass.name());
checker.no_change(); checker.no_change();
} }
} }
if checker.is_finished() { if checker.is_finished() {
break; break;
} }
@ -141,28 +167,55 @@ impl Minimizer {
Ok(()) Ok(())
} }
} }
#[derive(Clone, PartialEq, Eq, Hash)]
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct AstPath(Vec<String>); struct AstPath(Vec<String>);
impl Borrow<[String]> for AstPath { impl Borrow<[String]> for AstPath {
fn borrow(&self) -> &[String] { fn borrow(&self) -> &[String] {
&self.0 &self.0
} }
} }
impl Debug for AstPath { impl Debug for AstPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AstPath({:?})", self.0) write!(f, "AstPath({:?})", self.0)
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct PassController { pub(crate) struct PassController {
state: PassControllerState, state: PassControllerState,
} }
#[derive(Debug)] #[derive(Debug)]
enum PassControllerState { enum PassControllerState {
InitialCollection { candidates: Vec<AstPath> }, InitialCollection {
Bisecting { current: HashSet<AstPath>, worklist: Vec<Vec<AstPath>> }, candidates: Vec<AstPath>,
},
Bisecting {
committed: BTreeSet<AstPath>,
failed: BTreeSet<AstPath>,
current: BTreeSet<AstPath>,
worklist: Vec<Vec<AstPath>>,
},
Success, Success,
} }
fn split_owned<T, From: IntoIterator<Item = T>, A: FromIterator<T>, B: FromIterator<T>>(
vec: From,
) -> (A, B) {
let candidates = vec.into_iter().collect::<Vec<_>>();
let half = candidates.len() / 2;
let mut candidates = candidates.into_iter();
let first_half = candidates.by_ref().take(half).collect();
let second_half = candidates.collect();
(first_half, second_half)
}
impl PassController { impl PassController {
fn new() -> Self { fn new() -> Self {
Self { Self {
@ -171,41 +224,80 @@ impl PassController {
}, },
} }
} }
fn next_in_worklist(&mut self) {
match &mut self.state {
PassControllerState::Bisecting {
current, worklist, ..
} => match worklist.pop() {
Some(next) => {
*current = next.into_iter().collect();
}
None => {
self.state = PassControllerState::Success;
}
},
_ => unreachable!("next_in_worklist called on non-bisecting state"),
}
}
fn reproduces(&mut self) { fn reproduces(&mut self) {
match &mut self.state { match &mut self.state {
PassControllerState::InitialCollection { .. } => { PassControllerState::InitialCollection { .. } => {
self.state = PassControllerState::Success; self.state = PassControllerState::Success;
} }
PassControllerState::Bisecting { current, worklist, .. } => { PassControllerState::Bisecting {
match worklist.pop() { committed,
Some(next) => *current = next.into_iter().collect(), failed: _,
None => { current,
self.state = PassControllerState::Success; worklist: _,
} } => {
} committed.extend(mem::take(current));
self.next_in_worklist();
} }
PassControllerState::Success => unreachable!("Processed after success"), PassControllerState::Success => unreachable!("Processed after success"),
} }
} }
fn does_not_reproduce(&mut self) { fn does_not_reproduce(&mut self) {
match &mut self.state { match &mut self.state {
PassControllerState::InitialCollection { candidates } => { PassControllerState::InitialCollection { candidates } => {
let candidates = mem::take(candidates); let (current, first_worklist_item) = split_owned(mem::take(candidates));
let half = candidates.len() / 2;
let (first_half, second_half) = candidates.split_at(half); self.state = PassControllerState::Bisecting {
self committed: BTreeSet::new(),
.state = PassControllerState::Bisecting { failed: BTreeSet::new(),
current: first_half.iter().cloned().collect(), current,
worklist: vec![second_half.to_owned()], worklist: vec![first_worklist_item],
}; };
} }
PassControllerState::Bisecting { current, worklist } => { PassControllerState::Bisecting {
dbg!(& current, & worklist); committed,
todo!(); failed,
current,
worklist,
} => {
debug!(?committed, ?current, ?worklist);
if current.len() == 1 {
// We are at a leaf. This is a failure.
// FIXME: We should retry the failed ones until a fixpoint is reached.
failed.extend(mem::take(current));
} else {
// Split it further and add it to the worklist.
let (first_half, second_half) = split_owned(mem::take(current));
worklist.push(first_half);
worklist.push(second_half);
}
self.next_in_worklist()
} }
PassControllerState::Success => unreachable!("Processed after success"), PassControllerState::Success => unreachable!("Processed after success"),
} }
} }
fn no_change(&mut self) { fn no_change(&mut self) {
match &self.state { match &self.state {
PassControllerState::InitialCollection { candidates } => { PassControllerState::InitialCollection { candidates } => {
@ -216,13 +308,12 @@ impl PassController {
self.state = PassControllerState::Success; self.state = PassControllerState::Success;
} }
PassControllerState::Bisecting { current, .. } => { PassControllerState::Bisecting { current, .. } => {
unreachable!( unreachable!("No change while bisecting, current was empty somehow: {current:?}");
"No change while bisecting, current was empty somehow: {current:?}"
);
} }
PassControllerState::Success => {} PassControllerState::Success => {}
} }
} }
fn is_finished(&mut self) -> bool { fn is_finished(&mut self) -> bool {
match &mut self.state { match &mut self.state {
PassControllerState::InitialCollection { .. } => false, PassControllerState::InitialCollection { .. } => false,
@ -230,6 +321,7 @@ impl PassController {
PassControllerState::Success => true, PassControllerState::Success => true,
} }
} }
pub(crate) fn can_process(&mut self, path: &[String]) -> bool { pub(crate) fn can_process(&mut self, path: &[String]) -> bool {
match &mut self.state { match &mut self.state {
PassControllerState::InitialCollection { candidates } => { PassControllerState::InitialCollection { candidates } => {
@ -243,31 +335,42 @@ impl PassController {
} }
} }
} }
macro_rules! tracking { macro_rules! tracking {
() => { () => {
tracking!(visit_item_fn_mut); tracking!(visit_impl_item_method_mut); tracking!(visit_item_fn_mut);
tracking!(visit_item_impl_mut); tracking!(visit_item_mod_mut); tracking!(visit_impl_item_method_mut);
tracking!(visit_item_impl_mut);
tracking!(visit_item_mod_mut);
}; };
(visit_item_fn_mut) => { (visit_item_fn_mut) => {
fn visit_item_fn_mut(& mut self, func : & mut syn::ItemFn) { self.current_path fn visit_item_fn_mut(&mut self, func: &mut syn::ItemFn) {
.push(func.sig.ident.to_string()); syn::visit_mut::visit_item_fn_mut(self, func); self.current_path.push(func.sig.ident.to_string());
self.current_path.pop(); } syn::visit_mut::visit_item_fn_mut(self, func);
self.current_path.pop();
}
}; };
(visit_impl_item_method_mut) => { (visit_impl_item_method_mut) => {
fn visit_impl_item_method_mut(& mut self, method : & mut syn::ImplItemMethod) { fn visit_impl_item_method_mut(&mut self, method: &mut syn::ImplItemMethod) {
self.current_path.push(method.sig.ident.to_string()); self.current_path.push(method.sig.ident.to_string());
syn::visit_mut::visit_impl_item_method_mut(self, method); self.current_path syn::visit_mut::visit_impl_item_method_mut(self, method);
.pop(); } self.current_path.pop();
}
}; };
(visit_item_impl_mut) => { (visit_item_impl_mut) => {
fn visit_item_impl_mut(& mut self, item : & mut syn::ItemImpl) { self fn visit_item_impl_mut(&mut self, item: &mut syn::ItemImpl) {
.current_path.push(item.self_ty.clone().into_token_stream().to_string()); self.current_path
syn::visit_mut::visit_item_impl_mut(self, item); self.current_path.pop(); } .push(item.self_ty.clone().into_token_stream().to_string());
syn::visit_mut::visit_item_impl_mut(self, item);
self.current_path.pop();
}
}; };
(visit_item_mod_mut) => { (visit_item_mod_mut) => {
fn visit_item_mod_mut(& mut self, module : & mut syn::ItemMod) { self fn visit_item_mod_mut(&mut self, module: &mut syn::ItemMod) {
.current_path.push(module.ident.to_string()); self.current_path.push(module.ident.to_string());
syn::visit_mut::visit_item_mod_mut(self, module); self.current_path.pop(); } syn::visit_mut::visit_item_mod_mut(self, module);
self.current_path.pop();
}
}; };
} }
pub(crate) use tracking; pub(crate) use tracking;

View file

@ -23,7 +23,7 @@ fn file_for_suggestion(suggestion: &Suggestion) -> &str {
impl Minimizer { impl Minimizer {
pub fn delete_dead_code(&mut self) -> Result<()> { pub fn delete_dead_code(&mut self) -> Result<()> {
let inital_build = self.build.build()?; let inital_build = self.build.build()?;
println!("Before reaper: {inital_build}"); info!("Before reaper: {inital_build}");
inital_build.require_reproduction("Initial")?; inital_build.require_reproduction("Initial")?;
@ -80,7 +80,7 @@ impl Minimizer {
let after = self.build.build()?; let after = self.build.build()?;
println!("{}: After reaper: {after}", file.path.display()); info!("{}: After reaper: {after}", file.path.display());
if after.reproduces_issue() { if after.reproduces_issue() {
change.commit(); change.commit();

View file

@ -4,5 +4,4 @@ fn unused() {
fn main() { fn main() {
other::unused(); other::unused();
} }
mod other; mod other;