Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/mirth-lab/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use clap::{Parser, Subcommand};

mod tools {
pub mod abi_diff;
pub mod apply_suggestions;
pub mod audit_options;
pub mod callgraph;
pub mod coverage;
Expand All @@ -27,6 +28,7 @@ mod tools {
pub mod grammar_coverage;
pub mod instr_check;
pub mod miri_diff;
pub mod motivating;
pub mod opt_diff;
pub mod release_diff;
pub mod replay;
Expand All @@ -35,8 +37,10 @@ mod tools {
pub mod scale_check;
pub mod solver_diff;
pub mod suggest_diff;
pub mod survey;
pub mod ui_coverage;
pub mod ui_fuzz;
pub mod verify_closed;
pub mod xlink;
}

Expand Down Expand Up @@ -113,6 +117,18 @@ enum Check {
Replay(tools::replay::Args),
/// Coverage of the compiler across option configurations (a fixture per PICT transitions row).
CoverageFlags(tools::coverage_flags::Args),
/// Each machine-applicable warning suggestion in one file, applied alone: does it compile?
ApplySuggestions(tools::apply_suggestions::Args),
/// Reproduce docs/motivating's bugs on the toolchains before and after each fix.
MotivatingRun(tools::motivating::RunArgs),
/// Write docs/motivating.md from bugs.json, notes.json and the outputs.
MotivatingDoc(tools::motivating::DocArgs),
/// The last closed rust-lang/rust bugs and their fixing PRs, into issues.json.
SurveyFetch(tools::survey::FetchArgs),
/// Replace rollups in issues.json by the PR inside that names the issue.
SurveyUnroll(tools::survey::UnrollArgs),
/// Each closed-bug Ur query still finds the code its bug's fix changed.
VerifyClosed(tools::verify_closed::Args),
}

fn main() -> ExitCode {
Expand Down Expand Up @@ -150,6 +166,12 @@ fn main() -> ExitCode {
Check::FuzzReplay(a) => tools::fuzz_replay::run(a),
Check::Replay(a) => tools::replay::run(a),
Check::CoverageFlags(a) => tools::coverage_flags::run(a),
Check::ApplySuggestions(a) => tools::apply_suggestions::run(a),
Check::MotivatingRun(a) => tools::motivating::run(a),
Check::MotivatingDoc(a) => tools::motivating::doc(a),
Check::SurveyFetch(a) => tools::survey::fetch(a),
Check::SurveyUnroll(a) => tools::survey::unroll(a),
Check::VerifyClosed(a) => tools::verify_closed::run(a),
};
match result {
Ok(code) => code,
Expand Down
76 changes: 76 additions & 0 deletions crates/mirth-lab/src/tools/apply_suggestions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Each machine-applicable suggestion of a warning in one file, applied alone: does the result
//! still compile? (Finding 29's reductions; `suggest-diff` does this over every UI test.)
//!
//! Writes `<file>_fix<n>.rs` next to the file for the n-th suggestion and prints, per
//! suggestion, the lint code, the replaced text, the replacements and the first error.

use std::path::PathBuf;
use std::process::{Command, ExitCode};

use mirth_lab::rustc::diagnostics;

#[derive(clap::Args, Debug)]
pub struct Args {
file: PathBuf,
/// A rustup toolchain.
toolchain: String,
}

fn check(toolchain: &str, file: &str, json: bool) -> anyhow::Result<String> {
let mut cmd = Command::new("rustc");
cmd.arg(format!("+{toolchain}")).args(["--edition", "2021", "--emit=metadata"]);
if json {
cmd.arg("--error-format=json");
}
let out = cmd.args(["-o", "/dev/null", file]).output()?;
Ok(String::from_utf8_lossy(&out.stderr).into_owned())
}

/// A list of strings as Python prints it.
fn repr(items: &[String]) -> String {
let quoted: Vec<String> = items
.iter()
.map(|s| {
let q = if s.contains('\'') && !s.contains('"') { '"' } else { '\'' };
let body = s.replace('\\', "\\\\").replace('\n', "\\n");
let body = if q == '\'' { body.replace('\'', "\\'") } else { body };
format!("{q}{body}{q}")
})
.collect();
format!("[{}]", quoted.join(", "))
}

pub fn run(args: Args) -> anyhow::Result<ExitCode> {
let file = args.file.to_string_lossy().into_owned();
let src = std::fs::read(&args.file)?;
let mut n = 0;
for d in diagnostics(&check(&args.toolchain, &file, true)?) {
for c in std::iter::once(&d).chain(&d.children) {
let mut parts: Vec<(usize, usize, &str)> = c
.spans
.iter()
.filter(|s| s.suggestion_applicability.as_deref() == Some("MachineApplicable"))
.filter_map(|s| Some((s.byte_start, s.byte_end, s.suggested_replacement.as_deref()?)))
.collect();
if parts.is_empty() || d.level != "warning" {
continue;
}
n += 1;
let mut fixed = src.clone();
parts.sort_by(|a, b| b.cmp(a));
for &(a, b, t) in &parts {
fixed.splice(a..b, t.bytes());
}
parts.reverse();
let g = file.replacen(".rs", &format!("_fix{n}.rs"), 1);
std::fs::write(&g, &fixed)?;
let errs = check(&args.toolchain, &g, false)?;
let first = errs.lines().find(|l| l.starts_with("error")).unwrap_or("compiles");
let code = if d.code().is_empty() { "-" } else { d.code() };
let before: Vec<String> = parts.iter().map(|&(a, b, _)| String::from_utf8_lossy(&src[a..b]).into_owned()).collect();
let after: Vec<String> = parts.iter().map(|&(_, _, t)| t.to_owned()).collect();
println!(" {code}: {} -> {} => {first}", repr(&before), repr(&after));
}
}
Ok(ExitCode::SUCCESS)
}
152 changes: 152 additions & 0 deletions crates/mirth-lab/src/tools/motivating.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! docs/motivating.md: the real rust-lang/rust bugs behind each property, each reproduced on a
//! toolchain from before its fix and one after.
//!
//! `motivating-run [issue…]` runs the reproductions in docs/motivating/bugs.json and saves the
//! outputs to out/<issue>/{before,after}.txt. `motivating-doc` writes motivating.md from
//! bugs.json, notes.json (what each run showed, the replays on mirth, the prose) and the outputs.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::PathBuf;
use std::process::{Command, ExitCode};
use std::time::Duration;

use serde::Deserialize;

use mirth_lab::rustc::{Exit, run_command};

fn dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../docs/motivating")
}

#[derive(Deserialize)]
struct Bug {
issue: u64,
title: String,
fix_pr: u64,
merged: String,
before_toolchain: String,
after_toolchain: String,
properties: Vec<String>,
how_it_violates: String,
observe: String,
files: Vec<File>,
commands: String,
}

#[derive(Deserialize)]
struct File {
path: String,
content: String,
}

#[derive(Deserialize)]
struct Notes {
header: String,
/// Bugs only partly reproduced: what was seen (the rest are "reproduced").
status: BTreeMap<String, String>,
this_run: BTreeMap<String, String>,
mirth: BTreeMap<String, String>,
properties: Vec<Property>,
footer: String,
}

#[derive(Deserialize)]
struct Property {
key: String,
title: String,
why: String,
}

fn bugs() -> anyhow::Result<Vec<Bug>> {
Ok(serde_json::from_str(&std::fs::read_to_string(dir().join("bugs.json"))?)?)
}

#[derive(clap::Args, Debug)]
pub struct RunArgs {
/// Only these issues.
issues: Vec<u64>,
}

pub fn run(args: RunArgs) -> anyhow::Result<ExitCode> {
for bug in bugs()? {
if !args.issues.is_empty() && !args.issues.contains(&bug.issue) {
continue;
}
let out = dir().join("out").join(bug.issue.to_string());
std::fs::create_dir_all(&out)?;
for (side, toolchain) in [("before", &bug.before_toolchain), ("after", &bug.after_toolchain)] {
let installed = Command::new("rustup").args(["toolchain", "install", toolchain, "--profile", "minimal"]).output()?;
if !installed.status.success() {
std::fs::write(out.join(format!("{side}.txt")), format!("could not install {toolchain}\n"))?;
continue;
}
let d = tempfile::tempdir()?;
for f in &bug.files {
let path = d.path().join(&f.path);
std::fs::create_dir_all(path.parent().unwrap())?;
std::fs::write(&path, &f.content)?;
}
let mut cmd = Command::new("bash");
cmd.arg("-c")
.arg(bug.commands.replace("TOOLCHAIN", toolchain))
.current_dir(d.path())
.env("RUSTC_WRAPPER", "")
.env("CARGO_TERM_COLOR", "never");
let done = run_command(cmd, Duration::from_secs(900))?;
let text = match done.exit {
Exit::Timeout => format!("$ toolchain {toolchain}\ntimed out\n"),
Exit::Code(c) => format!("$ toolchain {toolchain}\n{}\n--- stderr ---\n{}\nexit {c}\n", done.stdout_text(), done.stderr_text()),
Exit::Signal(s) => format!("$ toolchain {toolchain}\n{}\n--- stderr ---\n{}\nexit -{s}\n", done.stdout_text(), done.stderr_text()),
};
std::fs::write(out.join(format!("{side}.txt")), text)?;
}
println!("#{}: done", bug.issue);
}
Ok(ExitCode::SUCCESS)
}

#[derive(clap::Args, Debug)]
pub struct DocArgs {}

pub fn doc(_: DocArgs) -> anyhow::Result<ExitCode> {
let bugs = bugs()?;
let notes: Notes = serde_json::from_str(&std::fs::read_to_string(dir().join("notes.json"))?)?;
let status = |b: &Bug| notes.status.get(&b.issue.to_string()).map_or("reproduced", String::as_str);
let mut out: Vec<String> = vec![notes.header.clone()];
for p in &notes.properties {
let rows: Vec<&Bug> = bugs.iter().filter(|b| b.properties.contains(&p.key)).collect();
out.push(format!("## {}: {}\n\n{}\n", p.key, p.title, p.why));
out.push("| issue | fixed by | merged | before → after | status | mirth, with the fix reverted |\n|---|---|---|---|---|---|".into());
for b in &rows {
let title: String = b.title.split(" (").next().unwrap_or("").chars().take(80).collect();
out.push(format!(
"| [#{0}](https://github.com/rust-lang/rust/issues/{0}) {title} | [#{1}](https://github.com/rust-lang/rust/pull/{1}) | {2} | `{3}` → `{4}` | {5} | {6} |",
b.issue,
b.fix_pr,
b.merged,
b.before_toolchain,
b.after_toolchain,
status(b).split(':').next().unwrap_or(""),
notes.mirth.get(&b.issue.to_string()).map_or("—", String::as_str)
));
}
out.push(String::new());
for b in &rows {
out.push(format!("**#{}.** {}\n", b.issue, b.how_it_violates));
if status(b) != "reproduced" {
out.push(format!("*Status:* {}.\n", status(b)));
}
let seen = notes.this_run.get(&b.issue.to_string()).map_or("", String::as_str);
let mut line = String::new();
let _ = write!(line, "*This run:* {seen}. Outputs: [`before`](motivating/out/{0}/before.txt), [`after`](motivating/out/{0}/after.txt).\n", b.issue);
out.push(line);
out.push(format!("*Expected, from the issue and the research:* {}\n", b.observe));
}
}
out.push(notes.footer.clone());
let text = out.join("\n");
std::fs::write(dir().join("../motivating.md"), &text)?;
println!("written {}", out.iter().map(|x| x.chars().count()).sum::<usize>());
Ok(ExitCode::SUCCESS)
}
Loading
Loading