compactp 0.1.0-beta.1

A production-grade parser frontend for the Compact language (Midnight Network)
use crate::Cli;
use crate::commands::cst::root_from_green;
use crate::error::CliError;
use crate::input::resolve_inputs;
use crate::output::OutputEnvelope;
use compactp_syntax::{SyntaxKind, SyntaxNode};
use serde::Serialize;
use std::path::PathBuf;
use std::time::Instant;

#[derive(Debug, Clone, Serialize)]
struct Stats {
    file_size_bytes: usize,
    token_count: usize,
    node_count: usize,
    error_count: usize,
    recovery_count: usize,
    parse_time_ms: f64,
}

pub fn run(cli: &Cli, paths: &[PathBuf]) -> Result<i32, CliError> {
    let inputs = resolve_inputs(paths, cli.stdin_filename.as_deref())?;
    let mut worst = 0;

    for input in inputs {
        let file_size = input.source.len();
        let token_count = compactp_lexer::lex(&input.source).len();

        let start = Instant::now();
        let result = compactp_parser::parse(&input.source);
        let parse_time = start.elapsed();

        let root = root_from_green(result.green);
        let node_count = count_nodes(&root);
        let recovery_count = count_error_nodes(&root);
        let error_count = result.errors.len();

        let stats = Stats {
            file_size_bytes: file_size,
            token_count,
            node_count,
            error_count,
            recovery_count,
            parse_time_ms: parse_time.as_secs_f64() * 1000.0,
        };

        match cli.format {
            crate::OutputFormat::Json => {
                let timing_ms = cli.timing.then_some(parse_time.as_secs_f64() * 1000.0);
                let envelope = OutputEnvelope::new(input.label.clone(), stats, timing_ms);
                crate::output::print_json(&envelope, cli.pretty)?;
            }
            crate::OutputFormat::Human => {
                println!("File:        {}", input.label);
                println!("Size:        {file_size} bytes");
                println!("Tokens:      {token_count}");
                println!("Nodes:       {node_count}");
                println!("Errors:      {error_count}");
                println!("Recoveries:  {recovery_count}");
                println!("Parse time:  {:.2}ms", stats.parse_time_ms);
            }
        }

        if error_count > 0 {
            worst = worst.max(1);
        }
    }

    Ok(worst)
}

fn count_nodes(node: &SyntaxNode) -> usize {
    let mut count = 1;
    for child in node.children() {
        count += count_nodes(&child);
    }
    count
}

fn count_error_nodes(root: &SyntaxNode) -> usize {
    root.descendants()
        .filter(|node| node.kind() == SyntaxKind::ERROR)
        .count()
}