saltwater 0.11.0

A C compiler written in Rust, with a focus on good error messages.
Documentation
#![allow(dead_code)]

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

extern crate env_logger;
extern crate log;
extern crate tempfile;

use log::info;
use saltwater::Error;

pub fn init() {
    env_logger::builder().is_test(true).init();
}

pub fn cpp() -> std::process::Command {
    let mut cpp = std::process::Command::new("cpp");
    cpp.args(&[
        "-P",
        "-undef",
        "-D__DBL_MAX__=1.797693134862315708e+308L",
        "-D__DBL_MIN__=2.225073858507201383e-308L",
        "-D__FLT_MAX__=3.402823466385288598e+38F",
        "-D__FLT_MIN__=1.175494350822287507e-38F",
        "-D__INTPTR_TYPE__=8",
        "-D__INT32_TYPE__=4",
        #[cfg(linux)]
        "-D__linux__",
        #[cfg(target_arch = "x86_64")]
        "-D__x86_64__",
    ]);
    cpp
}

pub fn compile_and_run(program: &str, path: PathBuf, args: &[&str]) -> Result<Output, Error> {
    let output = compile(program, path, false)
        .unwrap_or_else(|err| panic!("failed to compile program '{}': {}", program, err));
    info!("running file {:?}", output);
    run(&output, args).map_err(Error::IO)
}

pub fn compile(
    program: &str,
    filename: PathBuf,
    no_link: bool,
) -> Result<tempfile::TempPath, Error> {
    let opts = saltwater::Opt {
        filename,
        ..Default::default()
    };
    let module = saltwater::initialize_aot_module(program.to_owned());
    let module = saltwater::compile(module, program, opts).result?.finish();
    let output = tempfile::NamedTempFile::new()
        .expect("cannot create tempfile")
        .into_temp_path();
    info!("output is {:?}", output);
    if !no_link {
        let tmp_file = tempfile::NamedTempFile::new()
            .expect("cannot create tempfile")
            .into_temp_path();
        info!("tmp_file is {:?}", tmp_file);
        saltwater::assemble(module, &tmp_file)?;
        saltwater::link(&tmp_file, &output)?;
    } else {
        saltwater::assemble(module, &output)?;
    };
    Ok(output)
}

pub fn run(program: &Path, args: &[&str]) -> Result<Output, std::io::Error> {
    Command::new(program).args(args).output()
}

pub fn assert_compiles(program: &str, path: PathBuf) {
    assert!(
        compile(program, path, true).is_err(),
        "{} failed to compile",
        program
    );
}

pub fn assert_compiles_no_main(fragment: &str, path: PathBuf) {
    let program = format!("int main() {{}}\n{}", fragment);
    assert!(
        compile(&program, path, true).is_ok(),
        "{} failed to compile",
        fragment
    );
}

pub fn assert_compile_error(program: &str, path: PathBuf) {
    assert!(
        match compile(program, path, true) {
            Err(Error::Source(_)) => true,
            _ => false,
        },
        "{} should fail to compile",
        program
    );
}

pub fn assert_crash(program: &str, path: PathBuf) {
    let output = compile(program, path, false).expect("could not compile program");
    log::debug!("running compiled program at {:?}", output);
    let path: &Path = output.as_ref();
    let mut handle = Command::new(path)
        .spawn()
        .expect("could not start compiled program");
    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;
        assert!(handle
            .wait()
            .expect("call to libc::wait should succeed")
            .signal()
            .is_some());
    }
    #[cfg(not(unix))]
    {
        use log::warn;
        warn!("testing for segfaults is not supported on non-unix platforms, this just checks the return code is non-zero");
        assert!(!handle.wait().unwrap().success());
    }
}

pub fn assert_output(program: &str, path: PathBuf, output: &str) {
    match compile_and_run(program, path, &[]) {
        Err(_) => panic!("program failed to compile or run: {}", program),
        Ok(actual) => assert_eq!(
            actual.stdout,
            output.as_bytes(),
            "{} should have the output {} (got {})",
            program,
            output,
            String::from_utf8_lossy(&actual.stdout),
        ),
    }
}
pub fn assert_succeeds(program: &str, path: PathBuf) {
    assert!(
        match compile_and_run(program, path, &[]) {
            Err(_) => false,
            Ok(output) => output.status.success(),
        },
        "'{}' should exit successfully",
        program
    );
}

pub fn assert_code(program: &str, path: PathBuf, code: i32) {
    assert!(
        match compile_and_run(program, path, &[]) {
            Err(_) => false,
            Ok(output) => match output.status.code() {
                Some(actual) => actual == code,
                None => false,
            },
        },
        "{} should exit with code {}",
        program,
        code
    );
}

pub fn assert_num_errs<S: AsRef<str>>(program: S, path: PathBuf, n: usize) {
    match compile(program.as_ref(), path, true) {
        Err(Error::Source(errs)) => assert!(errs.len() == n),
        _ => panic!("program should have an error"),
    }
}