rexact-core 0.2.0

Derivative-based regex engine for Rexact: symbol classes, lazy DFA, minimisation, per-dialect semantics
Documentation
//! The Java dialect agrees with `java.util.regex` on every short string
//! (corpus/match_java.tsv from oracle/gen_java_corpus.py; RULE-13).
//! Rows our lowering rejects with RX-U007/U005 are counted, never silently passed.

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

use rexact_core::{Language, Limits, Mode};
use rexact_syntax::{Dialect, Flags, Pattern};
use std::collections::HashMap;

fn set(name: &str) -> (Vec<&'static str>, usize) {
    match name {
        "gen" => (
            vec!["a", "b", "c", "\u{e9}", "\n", "1", " ", "\u{1F600}"],
            4,
        ),
        "edge" => (
            vec![
                "0", "9", "/", ":", "_", "A", "@", "[", "z", "`", "\u{a0}", "\r", "\u{2028}",
                "\u{2029}", "\u{85}", "\u{feff}",
            ],
            3,
        ),
        "crlf" => (vec!["a", "\r", "\n", "\u{85}"], 4),
        other => panic!("unknown set {other}"),
    }
}

fn strings(symbols: &[&str], maxlen: usize) -> Vec<String> {
    let mut out = vec![String::new()];
    let mut layer = vec![String::new()];
    for _ in 0..maxlen {
        let mut next = Vec::new();
        for s in &layer {
            for sym in symbols {
                next.push(format!("{s}{sym}"));
            }
        }
        out.extend(next.iter().cloned());
        layer = next;
    }
    out
}

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

#[test]
fn java_matching_agrees_with_java_util_regex() {
    let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../corpus/match_java.tsv");
    let text = std::fs::read_to_string(path).expect("corpus readable");
    let mut all: HashMap<&str, Vec<String>> = HashMap::new();
    for name in ["gen", "edge", "crlf"] {
        let (syms, n) = set(name);
        all.insert(name, strings(&syms, n));
    }
    let limits = Limits::default();
    let (mut compared, mut skipped_u, mut rejected_by_java, mut checks) = (0, 0, 0, 0u64);
    let mut failures = Vec::new();
    for line in text
        .lines()
        .filter(|l| !l.starts_with('#') && !l.is_empty())
    {
        let c: Vec<&str> = line.split('\t').collect();
        let strs = &all[c[0]];
        let flags = Flags::from_letters(Dialect::Java, c[2]).unwrap().0;
        let pattern = decode(c[3]);
        let label = format!("{}/{}/{:?}", c[0], c[2], pattern);
        if c[4] == "-" {
            rejected_by_java += 1;
            // Java rejects the pattern: we must not accept it as a valid Java pattern.
            if let Ok(p) = Pattern::parse(&pattern, Dialect::Java, flags) {
                if Language::compile(&p, None, Mode::Full, &limits).is_ok() {
                    failures.push(format!("{label}: Java rejects, we accept"));
                }
            }
            continue;
        }
        let bytes: Vec<u8> = (0..c[4].len() / 2)
            .map(|i| u8::from_str_radix(&c[4][2 * i..2 * i + 2], 16).unwrap())
            .collect();
        let parsed = match Pattern::parse(&pattern, Dialect::Java, flags) {
            Ok(p) => p,
            Err(e) if e.code() == "RX-U007" || e.code() == "RX-U001" || e.code() == "RX-U004" => {
                skipped_u += 1;
                continue;
            }
            Err(e) => {
                failures.push(format!(
                    "{label}: Java accepts, parser rejects: {}",
                    e.code()
                ));
                continue;
            }
        };
        let mut lang = match Language::compile(&parsed, None, Mode::Full, &limits) {
            Ok(l) => l,
            Err(e) if e.code() == "RX-U007" || e.code() == "RX-U005" => {
                skipped_u += 1;
                continue;
            }
            Err(e) => {
                failures.push(format!("{label}: compile failed: {e}"));
                continue;
            }
        };
        compared += 1;
        for (i, s) in strs.iter().enumerate() {
            let expected = bytes[i / 8] >> (i % 8) & 1 == 1;
            let got = lang.matches(s).expect("match");
            checks += 1;
            if got != expected {
                failures.push(format!(
                    "{label}: input {s:?}: java={expected} rexact={got}"
                ));
                break;
            }
        }
    }
    eprintln!(
        "compared {compared} rows ({checks} checks); skipped {skipped_u} unmodelled; {rejected_by_java} rejected by Java"
    );
    assert!(
        failures.is_empty(),
        "{} disagreements:\n{}",
        failures.len(),
        failures
            .iter()
            .take(60)
            .cloned()
            .collect::<Vec<_>>()
            .join("\n")
    );
    assert!(compared > 500, "too few rows compared: {compared}");
}