use anyhow::{Context, bail};
use duct::cmd;
use geno::StandardFileResolver;
use std::{
cell::RefCell,
fs::{self, File},
io::{Write, stdout},
path::PathBuf,
process::exit,
rc::Rc,
};
#[derive(clap::Parser)]
#[command(
name = "geno",
version,
about = "Geno Structure Compiler",
long_about = "Geno is a compiler for generating source code for different languages from from generic structure definitions. It works by compiling a .geno file into an intermediate AST representation, then generating source code from that AST. It uses a pipeline model to generate code. Generators are simply executables with the name `geno-` followed by the format name (e.g. `geno-dart-json`). You can pass parameters to the generator by adding a `--` separator after the last `geno` argument and then the generator's parameters."
)]
struct Cli {
/// Input .geno file
#[arg(value_name = "INPUT_FILE")]
input_path: PathBuf,
/// Output file path for the generated source code, or STDOUT if not provided
#[arg(value_name = "OUTPUT_FILE", short = 'o', long)]
output_path: Option<PathBuf>,
/// Intermediate AST file path for debugging. Program will write the AST
/// to this file in MessagePack format then exit.
#[arg(value_name = "AST_FILE", short = 't', long)]
ast_path: Option<PathBuf>,
/// Output source code format (e.g. -f dart-json or -f rust-rmp)
#[arg(value_name = "FORMAT", short = 'f', long)]
format: Option<String>,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
extra_args: Vec<String>,
}
fn main() {
match run() {
Ok(code) => exit(code),
Err(err) => {
eprintln!("error: {}", err);
let mut source = err.source();
while let Some(e) = source {
eprintln!("caused by: {}", e);
source = e.source();
}
exit(1);
}
}
}
fn run() -> anyhow::Result<i32> {
use clap::Parser;
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
// This prints the error message from clap
eprintln!("{}", err);
return Ok(0);
}
};
// Parse the input string into an AST
let ast = geno::Parser::new(Rc::new(RefCell::new(StandardFileResolver::new())))
.parse(&cli.input_path)?;
// Validate the AST
ast.validate()?;
// If the user specified an AST output path, write the AST to that file and exit
if let Some(ast_path) = cli.ast_path {
let mut file = File::create(&ast_path).context(format!(
"Could not create AST file '{}'",
ast_path.to_string_lossy()
))?;
rmp_serde::encode::write(&mut file, &ast)
.context("Failed to serialize AST to MessagePack")?;
return Ok(0);
}
let format = match cli.format {
Some(s) => s,
None => bail!("No output format specified"),
};
let extra_args: Vec<&str> = cli.extra_args.iter().map(|s| s.as_str()).collect();
let cmd_expr = if std::env::var("GENO_DEBUG").is_ok() {
cmd(
"cargo",
itertools::concat(vec![
vec!["run", "--bin", &format!("geno-{}", format), "--"],
extra_args,
]),
)
} else {
cmd(format!("geno-{}", format), extra_args)
};
let ast_bytes = rmp_serde::to_vec(&ast).context("Failed to serialize AST to MessagePack")?;
let output = cmd_expr
.stdin_bytes(ast_bytes)
.stdout_capture()
.read()
.with_context(|| format!("Failed to run AST formatter '{:?}'", cmd_expr))?;
match cli.output_path {
Some(path) => {
fs::write(path, output)?;
}
None => {
stdout().write(output.as_bytes())?;
}
};
Ok(0)
}