use std::io::{self, Read, Write};
use std::process::ExitCode;
use tabnas_alchemy::{canonical, compile, format, parse_file, Renderer};
use tabnas_transduce::{Code, Fail, Limits, Metrics, ParserSource, Prune, SourceMode};
const USAGE: &str = "usage: alchemy canon|format|check|explain FILE\n alchemy run [--render csv|json] [--no-native] [--max-output-bytes N] PROGRAM INPUT\n (a FILE may be - for standard input)";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let worker = std::thread::Builder::new()
.name("alchemy".into())
.stack_size(tabnas_alchemy::STACK_BYTES)
.spawn(move || command(&args));
match worker {
Ok(thread) => thread
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic)),
Err(error) => {
report(&Fail::new(
Code::ResourceLimitExceeded,
format!("no thread could be started for the command: {error}"),
));
ExitCode::from(5)
}
}
}
fn command(args: &[String]) -> ExitCode {
match run(args) {
Ok(text) => {
let mut out = io::stdout().lock();
if out
.write_all(text.as_bytes())
.and_then(|()| out.flush())
.is_err()
{
return ExitCode::from(3);
}
ExitCode::SUCCESS
}
Err(exit) => {
report(&exit.fail);
ExitCode::from(exit.status)
}
}
}
fn report(fail: &Fail) {
let mut err = io::stderr().lock();
let _ = writeln!(err, "{}", fail.to_json()).and_then(|()| err.flush());
}
struct Exit {
fail: Fail,
status: u8,
}
impl From<Fail> for Exit {
fn from(fail: Fail) -> Exit {
let status = match fail.code {
Code::DslParseError
| Code::DslTypeError
| Code::StreamReused
| Code::StreamabilityUnknown => 2,
Code::ResourceLimitExceeded => 5,
Code::OutputFailed => 3,
Code::Aborted => 6,
_ => 1,
};
Exit { fail, status }
}
}
fn usage() -> Exit {
Exit {
fail: Fail::input(USAGE),
status: 2,
}
}
fn read(file: &str) -> Result<String, Exit> {
let mut text = String::new();
let outcome = if file == "-" {
io::stdin().lock().read_to_string(&mut text).map(|_| ())
} else {
std::fs::read_to_string(file).map(|read| text = read)
};
outcome.map_err(|error| Exit {
fail: Fail::input(format!("cannot read {file}: {error}")),
status: 2,
})?;
Ok(text)
}
fn run(args: &[String]) -> Result<String, Exit> {
let (command, rest) = args.split_first().ok_or_else(usage)?;
match command.as_str() {
"canon" | "format" | "check" | "explain" => {
let [file] = rest else {
return Err(usage());
};
let src = read(file)?;
match command.as_str() {
"canon" => {
let text = canonical(&parse_file(&src, file)?);
Ok(if text.is_empty() { text } else { text + "\n" })
}
"format" => Ok(format(&parse_file(&src, file)?)),
"check" => Ok(compile(&src, file).map(|_| String::new())?),
_ => Ok(compile(&src, file).map(|program| program.explain())?),
}
}
"run" => {
let options = RunOptions::parse(rest)?;
let src = read(&options.program)?;
let mut program = compile(&src, &options.program)?;
if !options.native {
program = program.with_native(false)?;
}
let input = read(&options.input)?;
let limits = Limits {
max_output_bytes: options.max_output_bytes,
..Limits::default()
};
execute(&program, &input, options.render, limits)?;
Ok(String::new())
}
_ => Err(usage()),
}
}
struct RunOptions {
render: Option<Renderer>,
native: bool,
max_output_bytes: Option<u64>,
program: String,
input: String,
}
impl RunOptions {
fn parse(args: &[String]) -> Result<RunOptions, Exit> {
let mut render = None;
let mut native = true;
let mut max_output_bytes = None;
let mut files = Vec::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--render" => {
let name = args.get(i + 1).ok_or_else(usage)?;
render = Some(Renderer::named(name).ok_or_else(|| Exit {
fail: Fail::input(format!("--render takes csv or json, not {name:?}")),
status: 2,
})?);
i += 2;
}
"--no-native" => {
native = false;
i += 1;
}
"--max-output-bytes" => {
let n = args.get(i + 1).ok_or_else(usage)?;
max_output_bytes = Some(n.parse::<u64>().map_err(|_| Exit {
fail: Fail::input(format!(
"--max-output-bytes takes a number of bytes, not {n:?}"
)),
status: 2,
})?);
i += 2;
}
other if other.starts_with("--") => return Err(usage()),
other => {
files.push(other.to_string());
i += 1;
}
}
}
let [program, input] = files.as_slice() else {
return Err(usage());
};
if program == "-" && input == "-" {
return Err(Exit {
fail: Fail::input("only one of PROGRAM and INPUT may be - (standard input)"),
status: 2,
});
}
Ok(RunOptions {
render,
native,
max_output_bytes,
program: program.clone(),
input: input.clone(),
})
}
}
fn execute(
program: &tabnas_alchemy::Program,
input: &str,
render: Option<Renderer>,
limits: Limits,
) -> Result<(), Fail> {
let metrics = Metrics::new();
let sink = program.sink(Box::new(io::stdout()), render, &limits, metrics.clone())?;
let prune = match program.row_selector() {
Some(selector) => Prune::Under(selector.clone()),
None => Prune::Never,
};
let (outcome, _) = ParserSource::new(tabnas_json::make(), input)
.grammar("json")
.mode(SourceMode::Incremental { prune })
.limits(limits)
.metrics(metrics.clone())
.run_owned(sink);
outcome.map(|_| ()).map_err(|fail| {
if Metrics::get(&metrics.output_bytes) > 0 && !fail.committed_output {
fail.committed()
} else {
fail
}
})
}