rexact-syntax 0.2.0

Regex parsers for ECMAScript, Python, Go, Rust, PCRE2 and Java dialects, with an error catalogue
Documentation
//! Differential test (L3): accept/reject must agree with the real engines.
//!
//! `corpus/syntax_accept.tsv` (ECMAScript, Python) and
//! `corpus/syntax_accept_re2.tsv` (Go, Rust, PCRE2) are produced by
//! `oracle/gen_syntax_corpus*.py` from the real engines; nothing in them comes
//! from this parser (RULE-13). Rules:
//! * engine rejects  -> the parser must reject;
//! * engine accepts  -> the parser must accept, or reject with an `RX-U…`
//!   code because the construct is deliberately unsupported (RULE-05).
//!
//! Intentional, documented gaps are listed in `KNOWN_GAPS`.

#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]

use rexact_syntax::{Dialect, Flags, Pattern};
use std::collections::BTreeMap;

/// (dialect, flags, pattern, reason). Engine rejects, parser accepts.
const KNOWN_GAPS: &[(&str, &str, &str, &str)] = &[];

fn decode(hex: &str) -> String {
    hex.split_whitespace()
        .map(|h| char::from_u32(u32::from_str_radix(h, 16).expect("hex")).expect("scalar"))
        .collect()
}

fn run_file(name: &str) -> (usize, usize, BTreeMap<String, Vec<String>>) {
    let path = format!("{}/../../corpus/{name}", env!("CARGO_MANIFEST_DIR"));
    let text = std::fs::read_to_string(path).expect("corpus readable");
    let (mut total, mut unsupported) = (0usize, 0usize);
    let mut failures: BTreeMap<String, Vec<String>> = BTreeMap::new();
    for line in text
        .lines()
        .filter(|l| !l.starts_with('#') && !l.is_empty())
    {
        let cols: Vec<&str> = line.split('\t').collect();
        let dialect = Dialect::from_name(cols[0]).expect("dialect");
        let letters = cols[1];
        let engine_accepts = cols[2] == "1";
        let pattern = decode(cols.get(3).copied().unwrap_or(""));
        let flags = Flags::from_letters(dialect, letters).expect("flags").0;
        total += 1;
        let key = format!("{}/{:?}", cols[0], letters);
        let label = format!("{:?}", pattern);
        match (engine_accepts, Pattern::parse(&pattern, dialect, flags)) {
            (true, Ok(_)) => {}
            (true, Err(e)) if e.code().starts_with("RX-U") => unsupported += 1,
            (true, Err(e)) => failures.entry(key).or_default().push(format!(
                "engine ACCEPTS, parser rejects {label}: {}",
                e.code()
            )),
            (false, Err(_)) => {}
            (false, Ok(_)) => {
                let known = KNOWN_GAPS
                    .iter()
                    .any(|(d, f, p, _)| *d == cols[0] && *f == letters && *p == pattern);
                if !known {
                    failures
                        .entry(key)
                        .or_default()
                        .push(format!("engine REJECTS, parser accepts {label}"));
                }
            }
        }
    }
    (total, unsupported, failures)
}

fn check(name: &str, min_rows: usize) {
    let (total, unsupported, failures) = run_file(name);
    assert!(total > min_rows, "corpus unexpectedly small: {total}");
    if !failures.is_empty() {
        let n: usize = failures.values().map(Vec::len).sum();
        let mut msg = format!("{n} disagreements out of {total} in {name}:\n");
        for (k, v) in &failures {
            msg.push_str(&format!("--- {k}: {} disagreements\n", v.len()));
            for l in v.iter().take(25) {
                msg.push_str(&format!("{l}\n"));
            }
        }
        panic!("{msg}");
    }
    eprintln!("{name}: {total} rows agree; {unsupported} deliberately unsupported");
}

#[test]
fn parser_agrees_with_real_engines() {
    check("syntax_accept.tsv", 100_000);
}

#[test]
fn go_rust_pcre2_parsers_agree_with_real_engines() {
    check("syntax_accept_re2.tsv", 100_000);
}