rust-rule-engine 1.18.0-alpha

A blazing-fast Rust rule engine with RETE algorithm, backward chaining inference, and GRL (Grule Rule Language) syntax. Features SIMD/zero-copy/parallel parsing, forward/backward chaining, pattern matching, O(1) rule indexing, TMS, expression evaluation, method calls, streaming with Redis state backend. Production-ready for business rules, expert systems, and real-time decision automation.
Documentation
use super::simd_search;
use super::zero_copy;
/// Parallel rule parsing for large GRL files
///
/// This module uses rayon for parallel parsing of GRL rules, dramatically
/// improving parse times for large rule sets.
///
/// Performance benefits:
/// - Near-linear scaling with CPU cores
/// - 4-8x faster on quad-core systems
/// - Efficient work stealing for uneven rule sizes
/// - Maintains parse order for deterministic results
use rayon::prelude::*;

/// Parse multiple rules in parallel
///
/// Takes a GRL file and splits it into rules, then parses each rule
/// in parallel using all available CPU cores.
pub fn parse_rules_parallel(grl_text: &str) -> Vec<ParsedRule> {
    // First, split into rules (single-threaded, very fast)
    let rule_slices = zero_copy::split_into_rules_zero_copy(grl_text);

    // Parse each rule in parallel
    rule_slices
        .par_iter()
        .filter_map(|rule| parse_single_rule(rule.text))
        .collect()
}

/// Parse rules in parallel with SIMD optimization
pub fn parse_rules_parallel_simd(grl_text: &str) -> Vec<ParsedRule> {
    // Split using SIMD (faster for large files)
    let rule_slices = simd_search::split_into_rules_simd(grl_text);

    // Parse in parallel
    rule_slices
        .par_iter()
        .filter_map(|rule_text| parse_single_rule(rule_text))
        .collect()
}

/// A fully parsed rule
#[derive(Debug, Clone)]
pub struct ParsedRule {
    pub name: String,
    pub salience: Option<i32>,
    pub condition: String,
    pub action: String,
    pub no_loop: bool,
    pub lock_on_active: bool,
}

/// Parse a single rule (used by parallel workers)
fn parse_single_rule(rule_text: &str) -> Option<ParsedRule> {
    // Extract rule name
    let header = zero_copy::parse_rule_header_zero_copy(rule_text)?;
    let name = header.name.to_string();

    // Find attributes section
    let after_header = &rule_text[header.consumed..];
    let attributes_end = after_header.find('{')?;
    let attributes = &after_header[..attributes_end];

    // Extract salience
    let salience = zero_copy::extract_salience_zero_copy(attributes);

    // Check for flags
    let no_loop = zero_copy::has_attribute_zero_copy(attributes, "no-loop");
    let lock_on_active = zero_copy::has_attribute_zero_copy(attributes, "lock-on-active");

    // Extract body
    let body_start = rule_text.find('{')?;
    let body_end = simd_search::find_matching_brace_simd(rule_text, body_start)?;
    let body = &rule_text[body_start + 1..body_end];

    // Parse when-then
    let when_then = zero_copy::parse_when_then_zero_copy(body)?;

    Some(ParsedRule {
        name,
        salience,
        condition: when_then.condition.to_string(),
        action: when_then.action.to_string(),
        no_loop,
        lock_on_active,
    })
}

/// Parse modules and rules in parallel
///
/// Separates modules from rules, then parses each in parallel
pub fn parse_modules_and_rules_parallel(grl_text: &str) -> (Vec<ParsedModule>, Vec<ParsedRule>) {
    // Split modules and rules (single-threaded)
    let (module_texts, rules_text) = split_modules_and_rules(grl_text);

    // Parse modules and rules in parallel using rayon's join
    let (modules, rules) = rayon::join(
        || parse_modules_parallel(&module_texts),
        || parse_rules_parallel(&rules_text),
    );

    (modules, rules)
}

/// A fully parsed module
#[derive(Debug, Clone)]
pub struct ParsedModule {
    pub name: String,
    pub export_policy: ExportPolicy,
    pub imports: Vec<Import>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum ExportPolicy {
    All,
    None,
    Specific(Vec<String>),
}

#[derive(Debug, Clone)]
pub struct Import {
    pub module_name: String,
    pub pattern: Option<String>,
}

/// Parse multiple modules in parallel
fn parse_modules_parallel(module_texts: &[String]) -> Vec<ParsedModule> {
    module_texts
        .par_iter()
        .filter_map(|text| parse_single_module(text))
        .collect()
}

/// Parse a single module (used by parallel workers)
fn parse_single_module(module_text: &str) -> Option<ParsedModule> {
    let module = zero_copy::parse_module_zero_copy(module_text)?;
    let name = module.name.to_string();
    let body = module.body;

    // Parse export policy
    #[allow(clippy::if_same_then_else)]
    let export_policy = if body.contains("export: all") {
        ExportPolicy::All
    } else if body.contains("export: none") {
        ExportPolicy::None
    } else {
        ExportPolicy::None // Default
    };

    // Parse imports (simple for now)
    let imports = Vec::new(); // TODO: implement import parsing

    Some(ParsedModule {
        name,
        export_policy,
        imports,
    })
}

// Helper functions

fn split_modules_and_rules(grl_text: &str) -> (Vec<String>, String) {
    let mut modules = Vec::new();
    let mut rules_text = String::new();
    let bytes = grl_text.as_bytes();
    let mut i = 0;
    let mut last_copy = 0;

    while i < bytes.len() {
        if let Some(offset) = memchr::memmem::find(&bytes[i..], b"defmodule ") {
            let abs_pos = i + offset;

            // Copy text before defmodule to rules
            if abs_pos > last_copy {
                rules_text.push_str(&grl_text[last_copy..abs_pos]);
            }

            // Find the opening brace
            if let Some(brace_offset) = memchr::memchr(b'{', &bytes[abs_pos..]) {
                let brace_abs = abs_pos + brace_offset;

                // Find matching closing brace
                if let Some(close_pos) = simd_search::find_matching_brace_simd(grl_text, brace_abs)
                {
                    let module_text = &grl_text[abs_pos..=close_pos];
                    modules.push(module_text.to_string());
                    i = close_pos + 1;
                    last_copy = i;
                    continue;
                }
            }
        }
        i += 1;
    }

    // Copy remaining text
    if last_copy < grl_text.len() {
        rules_text.push_str(&grl_text[last_copy..]);
    }

    (modules, rules_text)
}

/// Parallel chunked parsing for extremely large files
///
/// Splits the file into chunks and parses each chunk in parallel,
/// then combines the results. Best for files with 1000+ rules.
pub fn parse_rules_chunked_parallel(grl_text: &str, chunk_size: usize) -> Vec<ParsedRule> {
    // Split into rules first
    let rule_slices = zero_copy::split_into_rules_zero_copy(grl_text);

    // Process in parallel chunks
    rule_slices
        .par_chunks(chunk_size)
        .flat_map(|chunk| {
            chunk
                .iter()
                .filter_map(|rule| parse_single_rule(rule.text))
                .collect::<Vec<_>>()
        })
        .collect()
}

/// Adaptive parallel parsing
///
/// Automatically chooses the best parsing strategy based on file size:
/// - Small files (< 10 rules): Single-threaded
/// - Medium files (10-100 rules): Simple parallel
/// - Large files (100+ rules): Chunked parallel with SIMD
pub fn parse_rules_adaptive(grl_text: &str) -> Vec<ParsedRule> {
    // Quick estimate of rule count
    let rule_count_estimate = grl_text.matches("rule ").count();

    if rule_count_estimate < 10 {
        // Small file: single-threaded is faster (no thread overhead)
        let rule_slices = zero_copy::split_into_rules_zero_copy(grl_text);
        rule_slices
            .iter()
            .filter_map(|rule| parse_single_rule(rule.text))
            .collect()
    } else if rule_count_estimate < 100 {
        // Medium file: simple parallel
        parse_rules_parallel(grl_text)
    } else {
        // Large file: chunked parallel with SIMD
        let chunk_size = (rule_count_estimate / rayon::current_num_threads()).max(10);
        parse_rules_chunked_parallel(grl_text, chunk_size)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_single_rule() {
        let rule = r#"rule "TestRule" salience 10 {
            when X > 5
            then Y = 10
        }"#;

        let parsed = parse_single_rule(rule).unwrap();
        assert_eq!(parsed.name, "TestRule");
        assert_eq!(parsed.salience, Some(10));
        assert!(parsed.condition.contains("X > 5"));
        assert!(parsed.action.contains("Y = 10"));
    }

    #[test]
    fn test_parse_rules_parallel() {
        let grl = r#"
rule "Rule1" salience 10 { when X > 5 then Y = 10 }
rule "Rule2" salience 20 { when A < 3 then B = 7 }
rule "Rule3" { when C == 1 then D = 2 }
        "#;

        let rules = parse_rules_parallel(grl);
        assert_eq!(rules.len(), 3);
        assert_eq!(rules[0].name, "Rule1");
        assert_eq!(rules[1].name, "Rule2");
        assert_eq!(rules[2].name, "Rule3");
    }

    #[test]
    fn test_parse_rules_parallel_simd() {
        let grl = r#"
rule "Rule1" { when X > 5 then Y = 10 }
rule "Rule2" { when A < 3 then B = 7 }
        "#;

        let rules = parse_rules_parallel_simd(grl);
        assert_eq!(rules.len(), 2);
    }

    #[test]
    fn test_parse_with_no_loop() {
        let rule = r#"rule "TestRule" no-loop {
            when X > 5
            then Y = 10
        }"#;

        let parsed = parse_single_rule(rule).unwrap();
        assert!(parsed.no_loop);
        assert!(!parsed.lock_on_active);
    }

    #[test]
    fn test_parse_chunked_parallel() {
        let mut grl = String::new();
        for i in 0..50 {
            grl.push_str(&format!(
                r#"rule "Rule{}" {{ when X > {} then Y = {} }}"#,
                i,
                i,
                i * 2
            ));
            grl.push('\n');
        }

        let rules = parse_rules_chunked_parallel(&grl, 10);
        assert_eq!(rules.len(), 50);
    }

    #[test]
    fn test_adaptive_parsing_small() {
        let grl = r#"
rule "Rule1" { when X > 5 then Y = 10 }
rule "Rule2" { when A < 3 then B = 7 }
        "#;

        let rules = parse_rules_adaptive(grl);
        assert_eq!(rules.len(), 2);
    }

    #[test]
    fn test_adaptive_parsing_large() {
        let mut grl = String::new();
        for i in 0..150 {
            grl.push_str(&format!(
                r#"rule "Rule{}" {{ when X > {} then Y = {} }}"#,
                i,
                i,
                i * 2
            ));
            grl.push('\n');
        }

        let rules = parse_rules_adaptive(&grl);
        assert_eq!(rules.len(), 150);
    }

    #[test]
    fn test_parse_module() {
        let module_text = r#"defmodule MYMODULE {
            export: all
        }"#;

        let module = parse_single_module(module_text).unwrap();
        assert_eq!(module.name, "MYMODULE");
        assert_eq!(module.export_policy, ExportPolicy::All);
    }

    #[test]
    fn test_parse_modules_and_rules_parallel() {
        let grl = r#"
defmodule MODULE1 { export: all }
rule "Rule1" { when X > 5 then Y = 10 }
defmodule MODULE2 { export: none }
rule "Rule2" { when A < 3 then B = 7 }
        "#;

        let (modules, rules) = parse_modules_and_rules_parallel(grl);
        assert_eq!(modules.len(), 2);
        assert_eq!(rules.len(), 2);
    }
}