tree-sitter-perl-c 0.14.0

Tree-sitter Perl grammar binding (C FFI). Conventional C/tree-sitter reference implementation, kept alongside the native v3 parser for compatibility and comparison.
Documentation
use std::env;
use std::fs;
use std::time::Instant;
use tree_sitter_perl_c::{
    parse_perl_bytes, parse_perl_bytes_with_parser, parse_perl_code, parse_perl_code_with_parser,
    try_create_parser,
};

#[derive(Clone, Copy)]
enum Mode {
    Cold,
    Warm,
}

impl Mode {
    fn as_str(self) -> &'static str {
        match self {
            Self::Cold => "cold",
            Self::Warm => "warm",
        }
    }
}

#[derive(Clone, Copy)]
enum InputKind {
    Str,
    Bytes,
}

impl InputKind {
    fn as_str(self) -> &'static str {
        match self {
            Self::Str => "str",
            Self::Bytes => "bytes",
        }
    }
}

struct Config {
    file_path: String,
    mode: Mode,
    input: InputKind,
    iterations: u64,
}

struct BenchSummary {
    mode: Mode,
    input: InputKind,
    iterations: u64,
    total_us: u128,
    avg_us: u128,
    has_error: bool,
}

fn usage() -> &'static str {
    "Usage: bench_parser_c <file> [--mode cold|warm] [--iterations N] [--input str|bytes]\n\
     Defaults: --mode cold --iterations 1 --input str"
}

fn parse_config(args: &[String]) -> Result<Config, String> {
    if args.len() < 2 {
        return Err(usage().to_string());
    }

    let file_path = args[1].clone();
    let mut mode = Mode::Cold;
    let mut input = InputKind::Str;
    let mut iterations = 1_u64;

    let mut index = 2_usize;
    while index < args.len() {
        match args[index].as_str() {
            "--mode" => {
                let value =
                    args.get(index + 1).ok_or_else(|| "Missing value for --mode".to_string())?;
                mode = match value.as_str() {
                    "cold" => Mode::Cold,
                    "warm" => Mode::Warm,
                    _ => return Err(format!("Invalid mode: {value}")),
                };
                index += 2;
            }
            "--iterations" | "-n" => {
                let value = args
                    .get(index + 1)
                    .ok_or_else(|| "Missing value for --iterations".to_string())?;
                iterations = value
                    .parse::<u64>()
                    .map_err(|_| format!("Invalid iteration count: {value}"))?;
                if iterations == 0 {
                    return Err("--iterations must be greater than 0".to_string());
                }
                index += 2;
            }
            "--input" => {
                let value =
                    args.get(index + 1).ok_or_else(|| "Missing value for --input".to_string())?;
                input = match value.as_str() {
                    "str" => InputKind::Str,
                    "bytes" => InputKind::Bytes,
                    _ => return Err(format!("Invalid input mode: {value}")),
                };
                index += 2;
            }
            "--cold" => {
                mode = Mode::Cold;
                index += 1;
            }
            "--warm" => {
                mode = Mode::Warm;
                index += 1;
            }
            "--help" | "-h" => {
                return Err(usage().to_string());
            }
            unknown => {
                return Err(format!("Unknown argument: {unknown}"));
            }
        }
    }

    Ok(Config { file_path, mode, input, iterations })
}

fn parse_cold(input: InputKind, code: &[u8]) -> Result<bool, String> {
    let tree = match input {
        InputKind::Str => {
            let as_str = std::str::from_utf8(code)
                .map_err(|err| format!("Input is not UTF-8 for --input str: {err}"))?;
            parse_perl_code(as_str).map_err(|err| err.to_string())?
        }
        InputKind::Bytes => parse_perl_bytes(code).map_err(|err| err.to_string())?,
    };
    Ok(tree.root_node().has_error())
}

fn run_benchmark(config: &Config, code: &[u8]) -> Result<BenchSummary, String> {
    let mut saw_error = false;
    let start = Instant::now();

    match config.mode {
        Mode::Cold => {
            for _ in 0..config.iterations {
                if parse_cold(config.input, code)? {
                    saw_error = true;
                }
            }
        }
        Mode::Warm => {
            let mut parser = try_create_parser().map_err(|err| err.to_string())?;
            for _ in 0..config.iterations {
                let tree = match config.input {
                    InputKind::Str => {
                        let as_str = std::str::from_utf8(code)
                            .map_err(|err| format!("Input is not UTF-8 for --input str: {err}"))?;
                        parse_perl_code_with_parser(&mut parser, as_str)
                            .map_err(|err| err.to_string())?
                    }
                    InputKind::Bytes => parse_perl_bytes_with_parser(&mut parser, code)
                        .map_err(|err| err.to_string())?,
                };
                if tree.root_node().has_error() {
                    saw_error = true;
                }
            }
        }
    }

    let total_us = start.elapsed().as_micros();
    let avg_us = total_us / u128::from(config.iterations);

    Ok(BenchSummary {
        mode: config.mode,
        input: config.input,
        iterations: config.iterations,
        total_us,
        avg_us,
        has_error: saw_error,
    })
}

fn print_summary(summary: &BenchSummary) {
    println!("mode={}", summary.mode.as_str());
    println!("input={}", summary.input.as_str());
    println!("iterations={}", summary.iterations);
    println!("total_us={}", summary.total_us);
    println!("avg_us={}", summary.avg_us);
    println!("has_error={}", summary.has_error);
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let config = match parse_config(&args) {
        Ok(config) => config,
        Err(message) => {
            eprintln!("{message}");
            std::process::exit(1);
        }
    };

    let code = match fs::read(&config.file_path) {
        Ok(code) => code,
        Err(err) => {
            eprintln!("Failed to read file: {err}");
            std::process::exit(1);
        }
    };

    match run_benchmark(&config, &code) {
        Ok(summary) => {
            print_summary(&summary);
        }
        Err(err) => {
            eprintln!("Parse error: {err}");
            std::process::exit(1);
        }
    }
}