bcomp 0.1.0

A compiler for a subset of the BASIC language
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![deny(warnings)]
#![deny(missing_docs)]

//! A compiler for the basic language

use std::{
    fs,
    path::{Path, PathBuf},
};

use anyhow::Context;
use clap::Parser as ClapParser;

use crate::{emitter::Emitter, parser::Parser};

mod emitter;
mod lexer;
mod parser;

fn main() -> anyhow::Result<()> {
    let options = CompileOptions::parse();
    compile_file(&options)?;

    println!("wrote {}", options.output_path().to_string_lossy());

    Ok(())
}

#[derive(ClapParser)]
#[command(version, about)]
struct CompileOptions {
    /// BASIC source file to compile.
    input_path: PathBuf,

    /// Object file to write.
    #[arg(short, long)]
    output: Option<PathBuf>,
}

impl CompileOptions {
    fn output_path(&self) -> PathBuf {
        self.output
            .clone()
            .unwrap_or_else(|| default_output_path(&self.input_path))
    }
}

fn default_output_path(input_path: &Path) -> PathBuf {
    input_path.with_extension("o")
}

fn compile_file(options: &CompileOptions) -> anyhow::Result<()> {
    let source = fs::read_to_string(&options.input_path).with_context(|| {
        format!(
            "failed to read input file {}",
            options.input_path.to_string_lossy()
        )
    })?;

    let mut parser = Parser::from_input(&source);
    let program = parser.parse_program();
    let bytes = Emitter::new()?.emit_program(&program)?;

    let output_path = options.output_path();
    fs::write(&output_path, bytes).with_context(|| {
        format!(
            "failed to write output file {}",
            output_path.to_string_lossy()
        )
    })?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use clap::Parser;

    use crate::{CompileOptions, default_output_path};

    #[test]
    fn default_output_replaces_extension() {
        assert_eq!(
            default_output_path(PathBuf::from("tests/input/print.bsc").as_path()),
            PathBuf::from("tests/input/print.o")
        );
    }

    #[test]
    fn compile_options_use_default_output() -> anyhow::Result<()> {
        let options = CompileOptions::try_parse_from(["bcomp", "program.bsc"])?;

        assert_eq!(options.input_path, PathBuf::from("program.bsc"));
        assert_eq!(options.output_path(), PathBuf::from("program.o"));

        Ok(())
    }

    #[test]
    fn compile_options_accept_output_flag() -> anyhow::Result<()> {
        let options =
            CompileOptions::try_parse_from(["bcomp", "program.bsc", "-o", "build/program.o"])?;

        assert_eq!(options.input_path, PathBuf::from("program.bsc"));
        assert_eq!(options.output_path(), PathBuf::from("build/program.o"));

        Ok(())
    }
}