cslint-cli 0.1.0

CLI binary for the cslint constraint-system linter
//! CLI entry point for the cslint constraint-system linter.
//!
//! Reads an ACIR bytecode file, lowers it to the core IR, runs all four
//! heuristic analyses, and prints the results.

#![forbid(unsafe_code)]

use std::process::ExitCode;

use comp_cat_rs::effect::io::Io;

use cslint_core::diagnostic::Diagnostic;
use cslint_core::pipeline::analyze;

fn main() -> ExitCode {
    let pipeline = args_io()
        .flat_map(|path| {
            Io::suspend(move || {
                cslint_noir::load::load_and_lower(&path).map_err(CliError::Lower)
            })
        })
        .map(|graph| analyze(&graph))
        .map(|diags| render(&diags));

    match pipeline.run() {
        Ok(has_errors) => {
            if has_errors {
                ExitCode::from(1)
            } else {
                ExitCode::SUCCESS
            }
        }
        Err(e) => {
            eprintln!("error: {e}");
            ExitCode::from(2)
        }
    }
}

// ---------------------------------------------------------------------------
// CLI argument parsing (minimal, no clap dependency)
// ---------------------------------------------------------------------------

fn args_io() -> Io<CliError, String> {
    Io::suspend(|| {
        std::env::args()
            .nth(1)
            .ok_or_else(|| CliError::Usage("usage: cslint <file.acir>".into()))
    })
}

// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------

#[allow(clippy::needless_for_each)]
fn render(diags: &[Diagnostic]) -> bool {
    diags.iter().for_each(|d| {
        eprintln!("{d}");
    });
    !diags.is_empty()
}

// ---------------------------------------------------------------------------
// CLI error
// ---------------------------------------------------------------------------

#[derive(Debug)]
enum CliError {
    Usage(String),
    Lower(cslint_noir::error::Error),
}

impl std::fmt::Display for CliError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Usage(msg) => write!(f, "{msg}"),
            Self::Lower(e) => write!(f, "lowering failed: {e}"),
        }
    }
}

impl std::error::Error for CliError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Lower(e) => Some(e),
            Self::Usage(_) => None,
        }
    }
}