use super::simd_search;
use super::zero_copy;
use rayon::prelude::*;
pub fn parse_rules_parallel(grl_text: &str) -> Vec<ParsedRule> {
let rule_slices = zero_copy::split_into_rules_zero_copy(grl_text);
rule_slices
.par_iter()
.filter_map(|rule| parse_single_rule(rule.text))
.collect()
}
pub fn parse_rules_parallel_simd(grl_text: &str) -> Vec<ParsedRule> {
let rule_slices = simd_search::split_into_rules_simd(grl_text);
rule_slices
.par_iter()
.filter_map(|rule_text| parse_single_rule(rule_text))
.collect()
}
#[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,
}
fn parse_single_rule(rule_text: &str) -> Option<ParsedRule> {
let header = zero_copy::parse_rule_header_zero_copy(rule_text)?;
let name = header.name.to_string();
let after_header = &rule_text[header.consumed..];
let attributes_end = after_header.find('{')?;
let attributes = &after_header[..attributes_end];
let salience = zero_copy::extract_salience_zero_copy(attributes);
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");
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];
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,
})
}
pub fn parse_modules_and_rules_parallel(grl_text: &str) -> (Vec<ParsedModule>, Vec<ParsedRule>) {
let (module_texts, rules_text) = split_modules_and_rules(grl_text);
let (modules, rules) = rayon::join(
|| parse_modules_parallel(&module_texts),
|| parse_rules_parallel(&rules_text),
);
(modules, rules)
}
#[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>,
}
fn parse_modules_parallel(module_texts: &[String]) -> Vec<ParsedModule> {
module_texts
.par_iter()
.filter_map(|text| parse_single_module(text))
.collect()
}
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;
#[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 };
let imports = parse_imports(body);
Some(ParsedModule {
name,
export_policy,
imports,
})
}
fn parse_imports(body: &str) -> Vec<Import> {
let mut imports = Vec::new();
for line in body.lines() {
let trimmed = line.trim();
if let Some(spec) = trimmed.strip_prefix("import:") {
let spec = spec.trim();
if spec.is_empty() {
continue;
}
let (module_name, pattern) = if let Some(paren_start) = spec.find('(') {
let name = spec[..paren_start].trim();
let rest = &spec[paren_start..];
let pattern_content = rest
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.map(|s| s.trim().to_string());
(name, pattern_content)
} else {
(spec, None)
};
if !module_name.is_empty() {
imports.push(Import {
module_name: module_name.to_string(),
pattern,
});
}
}
}
imports
}
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;
if abs_pos > last_copy {
rules_text.push_str(&grl_text[last_copy..abs_pos]);
}
if let Some(brace_offset) = memchr::memchr(b'{', &bytes[abs_pos..]) {
let brace_abs = abs_pos + brace_offset;
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;
}
if last_copy < grl_text.len() {
rules_text.push_str(&grl_text[last_copy..]);
}
(modules, rules_text)
}
pub fn parse_rules_chunked_parallel(grl_text: &str, chunk_size: usize) -> Vec<ParsedRule> {
let rule_slices = zero_copy::split_into_rules_zero_copy(grl_text);
rule_slices
.par_chunks(chunk_size)
.flat_map(|chunk| {
chunk
.iter()
.filter_map(|rule| parse_single_rule(rule.text))
.collect::<Vec<_>>()
})
.collect()
}
pub fn parse_rules_adaptive(grl_text: &str) -> Vec<ParsedRule> {
let rule_count_estimate = grl_text.matches("rule ").count();
if rule_count_estimate < 10 {
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 {
parse_rules_parallel(grl_text)
} else {
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_imports_single() {
let body = "import: SENSORS (rules *)";
let imports = parse_imports(body);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].module_name, "SENSORS");
assert_eq!(imports[0].pattern.as_deref(), Some("rules *"));
}
#[test]
fn test_parse_imports_multiple() {
let body = r#"
export: all
import: SENSORS (rules *)
import: CONTROL (templates temperature)
"#;
let imports = parse_imports(body);
assert_eq!(imports.len(), 2);
assert_eq!(imports[0].module_name, "SENSORS");
assert_eq!(imports[0].pattern.as_deref(), Some("rules *"));
assert_eq!(imports[1].module_name, "CONTROL");
assert_eq!(
imports[1].pattern.as_deref(),
Some("templates temperature")
);
}
#[test]
fn test_parse_imports_no_pattern() {
let body = "import: UTILS";
let imports = parse_imports(body);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].module_name, "UTILS");
assert!(imports[0].pattern.is_none());
}
#[test]
fn test_parse_imports_empty_body() {
let body = "export: all";
let imports = parse_imports(body);
assert!(imports.is_empty());
}
#[test]
fn test_parse_module_with_imports() {
let module_text = r#"defmodule CONTROL {
export: all
import: SENSORS (rules *)
import: UTILS
}"#;
let module = parse_single_module(module_text).unwrap();
assert_eq!(module.name, "CONTROL");
assert_eq!(module.export_policy, ExportPolicy::All);
assert_eq!(module.imports.len(), 2);
assert_eq!(module.imports[0].module_name, "SENSORS");
assert_eq!(module.imports[0].pattern.as_deref(), Some("rules *"));
assert_eq!(module.imports[1].module_name, "UTILS");
assert!(module.imports[1].pattern.is_none());
}
#[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);
}
}