use std::ffi::OsStr;
use std::fs;
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode, Stdio};
use implicit_predicates_gen::{Position, generate, generate_points, parse, parse_points};
const USAGE: &str =
"usage: implicit-predicates-gen <pred-dir> --stages <dir> --checks <dir> [--check]";
struct Options {
definitions: PathBuf,
stages: PathBuf,
checks: PathBuf,
check: bool,
}
struct Output {
path: PathBuf,
contents: String,
}
fn main() -> ExitCode {
let options = match parse_args(std::env::args_os().skip(1)) {
Ok(options) => options,
Err(message) => {
eprintln!("{message}\n{USAGE}");
return ExitCode::from(2);
}
};
match run(&options) {
Ok(()) => ExitCode::SUCCESS,
Err(messages) => {
for message in messages {
eprintln!("{message}");
}
ExitCode::FAILURE
}
}
}
fn parse_args(args: impl Iterator<Item = std::ffi::OsString>) -> Result<Options, String> {
let (mut definitions, mut stages, mut checks, mut check) = (None, None, None, false);
let mut args = args;
while let Some(arg) = args.next() {
let mut value = |name: &str| {
args.next()
.map(PathBuf::from)
.ok_or_else(|| format!("{name} needs a directory"))
};
match arg.to_str() {
Some("--stages") => stages = Some(value("--stages")?),
Some("--checks") => checks = Some(value("--checks")?),
Some("--check") => check = true,
Some(flag) if flag.starts_with("--") => return Err(format!("unknown option {flag}")),
_ if definitions.is_none() => definitions = Some(PathBuf::from(arg)),
_ => return Err("only one directory of definitions is allowed".to_string()),
}
}
let options = Options {
definitions: definitions.ok_or("missing the directory of definitions")?,
stages: stages.ok_or("missing --stages")?,
checks: checks.ok_or("missing --checks")?,
check,
};
for path in [&options.definitions, &options.stages, &options.checks] {
if path.is_absolute() {
return Err(format!(
"{} is an absolute path: pass paths relative to the directory the command \
is run from, so the generated header is reproducible",
path.display()
));
}
}
Ok(options)
}
fn run(options: &Options) -> Result<(), Vec<String>> {
let outputs = outputs(options).map_err(|message| vec![message])?;
if options.check {
let stale: Vec<String> = outputs.iter().filter_map(compare).collect();
if stale.is_empty() { Ok(()) } else { Err(stale) }
} else {
for output in &outputs {
write(output).map_err(|message| vec![message])?;
}
Ok(())
}
}
fn outputs(options: &Options) -> Result<Vec<Output>, String> {
let dir = &options.definitions;
let entries = fs::read_dir(dir).map_err(|e| format!("cannot read {}: {e}", dir.display()))?;
let mut files = Vec::new();
for entry in entries {
let path = entry
.map_err(|e| format!("cannot read {}: {e}", dir.display()))?
.path();
if path.extension() == Some(OsStr::new("pred")) {
files.push(path);
}
}
files.sort();
if files.is_empty() {
return Err(format!("no .pred files in {}", dir.display()));
}
let label_dir = dir.file_name().map(Path::new).unwrap_or(Path::new(""));
let regenerate = regeneration_command(options);
let mut names: Vec<(String, PathBuf, bool)> = Vec::new();
let mut outputs = Vec::new();
for path in files {
let source = fs::read_to_string(&path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let label = label_dir.join(path.file_name().expect("a file"));
let in_file = |mut e: implicit_predicates_gen::Error| {
e.file = path.display().to_string();
e.to_string()
};
let label = label.to_string_lossy();
let mut generated = generate(&source, &label, ®enerate).map_err(in_file)?;
let predicates = generated.len();
generated.extend(generate_points(&source, &label, ®enerate).map_err(in_file)?);
for (i, g) in generated.into_iter().enumerate() {
let points = i >= predicates;
if let Some((_, other, other_points)) = names.iter().find(|(name, ..)| *name == g.name)
{
return Err(duplicate(
&path,
&source,
&g.name,
points,
other,
*other_points,
));
}
names.push((g.name.clone(), path.clone(), points));
let file = format!("{}.rs", g.name);
outputs.push(Output {
path: options.stages.join(&file),
contents: rustfmt(&g.stages)?,
});
if !g.checks.is_empty() {
outputs.push(Output {
path: options.checks.join(&file),
contents: rustfmt(&g.checks)?,
});
}
}
}
Ok(outputs)
}
fn duplicate(
path: &Path,
source: &str,
name: &str,
points: bool,
other: &Path,
other_points: bool,
) -> String {
let label = path.display().to_string();
let position = if points {
parse_points(source, &label)
.ok()
.and_then(|p| p.first().map(|p| p.position))
} else {
(parse(source, &label).ok())
.and_then(|p| p.into_iter().find(|p| p.name == name).map(|p| p.position))
};
let at = match position {
Some(Position { line, column }) => format!("{label}:{line}:{column}"),
None => label,
};
let other = other.display();
let message = match (points, other_points) {
(true, true) => format!(
"point types are also declared in {other}: the point types of a directory are \
declared in one file"
),
(true, false) => format!(
"the module of the point types, `{POINTS}`, has the name of the predicate \
`{POINTS}` of {other}"
),
(false, true) => format!(
"the predicate `{name}` has the name of the module of the point types, declared \
in {other}"
),
(false, false) => format!("the predicate `{name}` is also defined in {other}"),
};
format!("{at}: {message}")
}
const POINTS: &str = "points";
fn regeneration_command(options: &Options) -> String {
let mut command = String::from(env!("CARGO_BIN_NAME"));
let arguments = [
(None, &options.definitions),
(Some("--stages"), &options.stages),
(Some("--checks"), &options.checks),
];
for (flag, path) in arguments {
if let Some(flag) = flag {
command.push(' ');
command.push_str(flag);
}
command.push(' ');
command.push_str("ed(&path.to_string_lossy()));
}
format!("run from the directory that the paths are relative to:\n{command}")
}
fn quoted(argument: &str) -> String {
let plain = |c: char| c.is_ascii_alphanumeric() || "_-./=+,:@%".contains(c);
if !argument.is_empty() && argument.chars().all(plain) {
argument.to_string()
} else {
format!("'{}'", argument.replace('\'', "'\\''"))
}
}
fn rustfmt(code: &str) -> Result<String, String> {
let mut child = Command::new("rustfmt")
.args(["--edition", "2024"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| match e.kind() {
ErrorKind::NotFound => {
"cannot run rustfmt: it was not found; install it with `rustup component add rustfmt`"
.to_string()
}
_ => format!("cannot run rustfmt: {e}"),
})?;
let mut stdin = child.stdin.take().expect("stdin is piped");
let input = code.to_string();
let writer = std::thread::spawn(move || stdin.write_all(input.as_bytes()));
let output = child
.wait_with_output()
.map_err(|e| format!("cannot run rustfmt: {e}"))?;
writer
.join()
.expect("the writer does not panic")
.map_err(|e| format!("cannot write to rustfmt: {e}"))?;
if !output.status.success() {
return Err(format!(
"rustfmt rejected the generated code (a bug in the generator):\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
String::from_utf8(output.stdout).map_err(|_| "rustfmt wrote invalid UTF-8".to_string())
}
fn compare(output: &Output) -> Option<String> {
match fs::read(&output.path) {
Ok(contents) if contents == output.contents.as_bytes() => None,
Ok(_) => Some(format!(
"{} differs from the generated code",
output.path.display()
)),
Err(e) if e.kind() == ErrorKind::NotFound => {
Some(format!("{} is missing", output.path.display()))
}
Err(e) => Some(format!("cannot read {}: {e}", output.path.display())),
}
}
fn write(output: &Output) -> Result<(), String> {
let path = &output.path;
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).map_err(|e| format!("cannot create {}: {e}", dir.display()))?;
}
fs::write(path, &output.contents).map_err(|e| format!("cannot write {}: {e}", path.display()))
}