checksmix 0.4.3

An assembler, emulator and source-level debugger for Knuth's MMIX.
Documentation
/// MMIX Assembler - Compile .mms assembly files to .mmo object code
use checksmix::MMixAssembler;
use clap::Parser;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process;
use tracing_subscriber::{EnvFilter, fmt};

/// The status of a process whose stdout reader has gone: 128 plus SIGPIPE,
/// what a shell reports for a C tool that SIGPIPE killed.
const BROKEN_PIPE_STATUS: i32 = 141;

/// Ends the process after a failed write to stdout: quietly with
/// `BROKEN_PIPE_STATUS` once the reader has gone, else with the error on
/// stderr and status 1.
fn stdout_failed(err: &io::Error) -> ! {
    if err.kind() == io::ErrorKind::BrokenPipe {
        process::exit(BROKEN_PIPE_STATUS);
    }
    eprintln!("error writing to stdout: {err}");
    process::exit(1);
}

macro_rules! outln {
    ($($arg:tt)*) => {
        if let Err(err) = writeln!(io::stdout(), $($arg)*) {
            stdout_failed(&err);
        }
    };
}

#[derive(Parser, Debug)]
#[command(
    name = "mmixasm",
    about = "Assemble MMIX .mms files into .mmo object code",
    version,
    author
)]
struct Cli {
    /// Input MMIX assembly file(s) (.mms). Multiple inputs are loaded into
    /// one shared symbol space, as if their contents were concatenated.
    #[arg(value_name = "INPUT.mms", required = true, num_args = 1..)]
    inputs: Vec<PathBuf>,

    /// Output MMO file (default: first input's path with .mmo extension,
    /// beside the source)
    #[arg(short = 'o', long = "output", value_name = "OUTPUT.mmo")]
    output: Option<PathBuf>,
}

fn main() {
    // Initialize tracing subscriber with RUST_LOG environment variable support
    // By default, no debug output unless RUST_LOG is set
    // Example: RUST_LOG=checksmix=debug cargo run --bin mmixasm -- file.mms
    fmt().with_env_filter(EnvFilter::from_default_env()).init();

    let cli = Cli::parse();
    let inputs = cli.inputs;
    let output_file = cli
        .output
        .unwrap_or_else(|| inputs[0].with_extension("mmo"));

    let reader = |p: &Path| std::fs::read_to_string(p);
    let mut sources: Vec<(String, String)> = Vec::new();
    for path in &inputs {
        let src = fs::read_to_string(path).unwrap_or_else(|err| {
            eprintln!("Error reading '{}': {}", path.display(), err);
            process::exit(1);
        });
        let name = path
            .to_str()
            .map(|s| s.to_string())
            .unwrap_or_else(|| path.display().to_string());
        let base = path.parent().unwrap_or_else(|| Path::new("."));
        let units =
            MMixAssembler::resolve_includes(&src, &name, base, &reader).unwrap_or_else(|err| {
                eprintln!("{}", err);
                process::exit(1);
            });
        // A trimmed-away segment (blank space, or an INCLUDE resolving to
        // nothing) leaves this input with no translation unit at all.
        if units.is_empty() {
            eprintln!("Error: '{}' contributed no source", path.display());
            process::exit(1);
        }
        sources.extend(units);
    }

    if sources.len() == 1 {
        outln!("Assembling: {}", sources[0].0);
    } else {
        outln!("Assembling {} inputs:", sources.len());
        for (n, _) in &sources {
            outln!("  {}", n);
        }
    }

    let (first_name, first_src) = &sources[0];
    let mut assembler = MMixAssembler::new(first_src, first_name);
    for (n, s) in sources.iter().skip(1) {
        assembler.add_source(s, n);
    }

    if let Err(e) = assembler.parse() {
        eprintln!("{}", e);
        process::exit(1);
    }
    for warning in assembler.warnings() {
        eprintln!("{}", warning);
    }

    // Check if there are any instructions to assemble
    if assembler.instructions.is_empty() {
        eprintln!("Error: No instructions to assemble");
        process::exit(1);
    }

    // Generate object code
    let object_code = assembler.generate_object_code();

    outln!("Generated {} bytes of object code", object_code.len());

    // Write the output file
    fs::write(&output_file, &object_code).unwrap_or_else(|err| {
        eprintln!("Error writing '{}': {}", output_file.display(), err);
        process::exit(1);
    });

    outln!("Output written to: {}", output_file.display());
}