use clap::{CommandFactory, Parser, Subcommand};
use std::path::PathBuf;
use std::process::ExitCode;
fn run_script(source: &str, args: &[String]) -> ExitCode {
let dir = match tempfile::tempdir() {
Ok(d) => d,
Err(e) => {
eprintln!("error: cannot create temp dir: {e}");
return ExitCode::from(3);
}
};
let bin = dir.path().join("plg-script");
let src = std::path::Path::new(source);
if let Err(e) = plgc::compile_files(&[src], &bin, false, plgc::OptLevel::O3) {
eprintln!("error: {e}");
return ExitCode::from(3);
}
match std::process::Command::new(&bin).args(args).status() {
Ok(status) => ExitCode::from(status.code().unwrap_or(3) as u8),
Err(e) => {
eprintln!("error: failed to run compiled script: {e}");
ExitCode::from(3)
}
}
}
#[derive(Parser)]
#[command(
name = "plgc",
version,
about = "Compile ISO-subset Prolog to standalone native binaries"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Build {
inputs: Vec<PathBuf>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
keep_ir: bool,
#[arg(long)]
debug: bool,
#[arg(long)]
deny_undefined: bool,
},
Run {
inputs: Vec<PathBuf>,
#[arg(long)]
query: String,
#[arg(long)]
limit: Option<usize>,
#[arg(long, default_value = "text")]
format: String,
#[arg(long)]
deny_undefined: bool,
},
Check {
inputs: Vec<PathBuf>,
#[arg(long)]
deny_undefined: bool,
},
Completions {
shell: clap_complete::Shell,
},
}
fn lint_undefined(sources: &[&std::path::Path], deny: bool) -> Result<(), u8> {
let Ok(lints) = plgc::undefined_predicate_lints(sources) else {
return Ok(());
};
let label = if deny { "error" } else { "warning" };
for m in &lints {
eprintln!("{label}: {m}");
}
if deny && !lints.is_empty() {
return Err(2);
}
Ok(())
}
fn is_parse_error(msg: &str) -> bool {
msg.match_indices(": ").any(|(end, _)| {
let head = &msg[..end];
let Some((rest, col)) = head.rsplit_once(':') else {
return false;
};
let Some((_, line)) = rest.rsplit_once(':') else {
return false;
};
!col.is_empty()
&& col.bytes().all(|b| b.is_ascii_digit())
&& !line.is_empty()
&& line.bytes().all(|b| b.is_ascii_digit())
})
}
fn main() -> ExitCode {
let raw: Vec<String> = std::env::args().collect();
if raw.len() >= 2 && raw[1].ends_with(".pl") && std::path::Path::new(&raw[1]).exists() {
return run_script(&raw[1], &raw[2..]);
}
let cli = Cli::parse();
match cli.command {
Commands::Build {
inputs,
output,
keep_ir,
debug,
deny_undefined,
} => {
if inputs.is_empty() {
eprintln!("error: no input files");
return ExitCode::from(3);
}
let output =
output.unwrap_or_else(|| PathBuf::from(inputs[0].file_stem().unwrap_or_default()));
let sources: Vec<&std::path::Path> = inputs.iter().map(|p| p.as_path()).collect();
if let Err(code) = lint_undefined(&sources, deny_undefined) {
return ExitCode::from(code);
}
let opt = if debug {
plgc::OptLevel::O0
} else {
plgc::OptLevel::O3
};
match plgc::compile_files(&sources, &output, keep_ir, opt) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(3)
}
}
}
Commands::Run {
inputs,
query,
limit,
format,
deny_undefined,
} => {
if inputs.is_empty() {
eprintln!("error: no input files");
return ExitCode::from(3);
}
let sources: Vec<&std::path::Path> = inputs.iter().map(|p| p.as_path()).collect();
if let Err(code) = lint_undefined(&sources, deny_undefined) {
return ExitCode::from(code);
}
let dir = match tempfile::tempdir() {
Ok(d) => d,
Err(e) => {
eprintln!("error: cannot create temp dir: {e}");
return ExitCode::from(3);
}
};
let bin = dir.path().join("plg-run");
if let Err(e) = plgc::compile_files(&sources, &bin, false, plgc::OptLevel::O0) {
eprintln!("error: {e}");
let code = if is_parse_error(&e) { 2 } else { 3 };
return ExitCode::from(code);
}
let mut cmd = std::process::Command::new(&bin);
cmd.arg("--query").arg(&query).arg("--format").arg(&format);
if let Some(l) = limit {
cmd.arg("--limit").arg(l.to_string());
}
match cmd.status() {
Ok(status) => ExitCode::from(status.code().unwrap_or(3) as u8),
Err(e) => {
eprintln!("error: failed to run compiled binary: {e}");
ExitCode::from(3)
}
}
}
Commands::Check {
inputs,
deny_undefined,
} => {
let sources: Vec<&std::path::Path> = inputs.iter().map(|p| p.as_path()).collect();
match plgc::check_files(&sources) {
Ok(()) => match lint_undefined(&sources, deny_undefined) {
Ok(()) => ExitCode::SUCCESS,
Err(code) => ExitCode::from(code),
},
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(2)
}
}
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
ExitCode::SUCCESS
}
}
}