Files
nix-js/fix/src/main.rs
T

167 lines
4.8 KiB
Rust

use std::path::PathBuf;
use std::process::exit;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use fix::Evaluator;
use fix_error::Source;
use hashbrown::HashSet;
use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
#[derive(Parser)]
#[command(name = "fix", about = "Nix expression evaluator")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Compile {
#[clap(flatten)]
source: ExprSourceArgs,
#[arg(long)]
silent: bool,
},
Eval {
#[clap(flatten)]
source: ExprSourceArgs,
},
Repl,
}
#[derive(Args)]
#[group(required = true, multiple = false)]
struct ExprSourceArgs {
#[clap(short, long)]
expr: Option<String>,
#[clap(short, long)]
file: Option<PathBuf>,
}
enum ExprSource {
Expr(String),
File(PathBuf),
}
#[expect(
clippy::unreachable,
reason = "clap's arg group guarantees exactly one of --expr/--file is set"
)]
impl From<ExprSourceArgs> for ExprSource {
fn from(args: ExprSourceArgs) -> Self {
match (args.expr, args.file) {
(Some(expr), None) => ExprSource::Expr(expr),
(None, Some(file)) => ExprSource::File(file),
_ => unreachable!(),
}
}
}
fn run_compile(eval: &mut Evaluator, src: ExprSource, silent: bool) -> Result<()> {
let src = match src {
ExprSource::Expr(expr) => Source::new_eval(expr)?,
ExprSource::File(file) => Source::new_file(file)?,
};
match eval.compile_bytecode(src) {
Ok(ip) => {
if !silent {
println!("{}", eval.disassemble_colored(ip));
}
}
Err(err) => {
eprintln!("{:?}", miette::Report::new(*err));
exit(1);
}
};
Ok(())
}
fn run_eval(eval: &mut Evaluator, src: ExprSource) -> Result<()> {
let src = match src {
ExprSource::Expr(expr) => Source::new_eval(expr)?,
ExprSource::File(file) => Source::new_file(file)?,
};
match eval.eval_deep(src) {
Ok(value) => {
println!("{}", value.display_compat());
}
Err(err) => {
eprintln!("{:?}", miette::Report::new(*err));
exit(1);
}
};
Ok(())
}
fn run_repl(eval: &mut Evaluator) -> Result<()> {
let mut rl = DefaultEditor::new()?;
let mut scope = HashSet::new();
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
loop {
let readline = rl.readline("fix-repl> ");
match readline {
Ok(line) => {
if line.trim().is_empty() {
continue;
}
if let Err(err) = rl.add_history_entry(line.as_str()) {
eprintln!("[WARN] Failed to add history entry: {err}");
}
if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) {
if let Some(expr) = rest.strip_prefix('=') {
let expr = expr.trim_start();
if expr.is_empty() {
eprintln!("Error: missing expression after '='");
continue;
}
match eval.add_binding(ident, expr, &mut scope) {
Ok(value) => println!("{} = {}", ident, value),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
} else {
let src = Source::new_repl(line)?;
match eval.eval_repl(src, &scope) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
}
} else {
let src = Source::new_repl(line)?;
match eval.eval_repl(src, &scope) {
Ok(value) => println!("{value}"),
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
}
}
}
Err(ReadlineError::Interrupted) => {
println!();
}
Err(ReadlineError::Eof) => {
println!("CTRL-D");
break;
}
Err(err) => {
eprintln!("Error: {err:?}");
break;
}
}
}
Ok(())
}
fn main() -> Result<()> {
fix::logging::init_logging()?;
let cli = Cli::parse();
let mut eval = Evaluator::new();
match cli.command {
Command::Compile { source, silent } => run_compile(&mut eval, source.into(), silent),
Command::Eval { source } => run_eval(&mut eval, source.into()),
Command::Repl => run_repl(&mut eval),
}
}