cranefack-cli 0.4.2

Commandline utility for the cranefack brainfuck compiler
use clap::{crate_description, crate_name, crate_version, Arg, ArgMatches, Command, ArgAction};
use std::error::Error;
use std::ffi::OsString;

mod benchmark;
mod compile;
mod errors;
mod run;
mod utils;

use crate::benchmark::benchmark_file;
use crate::compile::compile_file;
use crate::run::run_file;
use crate::utils::get_optimize_config_from_args;
use std::process::exit;
use clap::builder::ValueParser;

fn main() {
    let matches = create_clap_app().get_matches();

    if let Err(error) = match matches.subcommand() {
        Some(("run", arg_matches)) => run(arg_matches),
        Some(("compile", arg_matches)) => compile(arg_matches),
        Some(("benchmark", arg_matches)) => benchmark(arg_matches),
        _ => {
            eprintln!("Missing command");
            Ok(())
        }
    } {
        eprintln!("{}", error);
        exit(1)
    }
}

fn create_clap_app() -> Command {
    Command::new(crate_name!())
        .version(crate_version!())
        .about(crate_description!())
        .subcommand(
            Command::new("run")
                .about("Run application")
                .arg(get_source_file())
                .arg(
                    Arg::new("JIT")
                        .short('j')
                        .long("jit")
                        .help("Use JIT compiler")
                        .action(ArgAction::SetTrue),
                )
                .arg(get_opt_mode_arg())
                .arg(get_jit_level())
                .arg(get_wrapping_is_ub_arg())
                .arg(get_debug_opt_arg())
                .arg(get_verbose_arg()),
        )
        .subcommand(
            Command::new("compile")
                .about("Compile application")
                .arg(get_source_file())
                .arg(
                    Arg::new("FORMAT")
                        .short('f')
                        .long("format")
                        .value_parser(["dump", "clir", "rust"])
                        .value_names(["format"])
                        .default_value("dump")
                        .help("Format of compiled code"),
                )
                .arg(get_opt_mode_arg())
                .arg(get_jit_level())
                .arg(get_wrapping_is_ub_arg())
                .arg(get_debug_opt_arg())
                .arg(get_verbose_arg()),
        )
        .subcommand(
            Command::new("benchmark")
                .about("Benchmark program with different optimization settings")
                .arg(get_source_file())
                .arg(
                    Arg::new("ITERATIONS")
                        .short('i')
                        .long("iterations")
                        .default_value("2")
                        .help("Number of benchmarking iterations"),
                )
                .arg(
                    Arg::new("RUNS")
                        .short('r')
                        .long("runs")
                        .default_value("4")
                        .help("Number of runs per optimization in each round"),
                )
                .arg(
                    Arg::new("OPTIMIZED_ONLY")
                        .short('o')
                        .long("optimized-only")
                        .help("Don't benchmark O0")
                        .action(ArgAction::SetTrue),
                )
                .arg(
                    Arg::new("JIT_ONLY")
                        .short('j')
                        .long("jit")
                        .help("Only benchmark jit")
                        .action(ArgAction::SetTrue),
                ),
        )
}

fn get_source_file() -> Arg {
    Arg::new("FILE")
        .required(true)
        .value_parser(ValueParser::os_string())
        .help("Brainfuck source file. Use - to read from stdin")
}

fn get_jit_level() -> Arg {
    Arg::new("JIT_LEVEL")
        .long("jit-level")
        .value_parser(["none", "speed", "speed_and_size"])
        .value_names(["level"])
        .help("Optimization level for JIT")
}

fn get_verbose_arg() -> Arg {
    Arg::new("VERBOSE")
        .short('v')
        .long("verbose")
        .action(ArgAction::SetTrue)
}

fn get_opt_mode_arg() -> Arg {
    Arg::new("OPT_MODE")
        .short('O')
        .value_parser(["0", "1", "2", "3", "s", "wtf"])
        .value_names(["mode"])
        .default_value("2")
        .help("Optimization mode")
}

fn get_wrapping_is_ub_arg() -> Arg {
    Arg::new("WRAPPING_IS_UB")
        .long("wrapping-is-ub")
        .help("Wrapping overflows are undefined behavior during optimization")
}

fn get_debug_opt_arg() -> Arg {
    Arg::new("DEBUG_OPT")
        .long("debug-optimizations")
        .help("Print statistics for optimization passes")
}

fn is_verbose(matches: &ArgMatches) -> bool {
    matches.contains_id("VERBOSE")
}

fn run(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let path: &OsString = matches.get_one("FILE").unwrap();
    let verbose = is_verbose(matches);
    let opt_mode = get_optimize_config_from_args(matches);
    let jit = matches.contains_id("JIT");

    run_file(opt_mode, jit, verbose, path)
}

fn compile(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let path: &OsString = matches.get_one("FILE").unwrap();
    let verbose = is_verbose(matches);
    let opt_mode = get_optimize_config_from_args(matches);
    let format = matches.get_one::<String>("FORMAT").map(|s| s.as_str()).unwrap_or("dump");

    compile_file(opt_mode, verbose, format, path)
}

fn benchmark(matches: &ArgMatches) -> Result<(), Box<dyn Error>> {
    let path: &OsString = matches.get_one("FILE").unwrap();

    let iterations = matches
        .get_one::<String>("ITERATIONS")
        .map(|s| s.as_str())
        .unwrap_or("2")
        .parse()
        .unwrap_or(2)
        .max(1);

    let runs = matches
        .get_one::<String>("RUNS")
        .map(|s| s.as_str())
        .unwrap_or("4")
        .parse()
        .unwrap_or(4)
        .max(1);

    let optimized_only = matches.contains_id("OPTIMIZED_ONLY");
    let jit_only = matches.contains_id("JIT_ONLY");

    benchmark_file(path, iterations, runs, optimized_only, jit_only)
}