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, #[clap(short, long)] file: Option, } enum ExprSource { Expr(String), File(PathBuf), } #[expect( clippy::unreachable, reason = "clap's arg group guarantees exactly one of --expr/--file is set" )] impl From 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), } }