1use std::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct RuleHeader<'a> {
17 pub name: &'a str,
19 pub full_text: &'a str,
21 pub consumed: usize,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct WhenThen<'a> {
28 pub condition: &'a str,
30 pub action: &'a str,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct Operator<'a> {
37 pub op: &'a str,
39 pub consumed: usize,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct Module<'a> {
46 pub name: &'a str,
48 pub body: &'a str,
50 pub consumed: usize,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq)]
56pub struct Rule<'a> {
57 pub text: &'a str,
59 pub start: usize,
61 pub end: usize,
63}
64
65pub fn parse_rule_header_zero_copy(text: &str) -> Option<RuleHeader<'_>> {
69 let trimmed = text.trim_start();
70
71 if !trimmed.starts_with("rule") {
72 return None;
73 }
74
75 let skip = text.len() - trimmed.len();
76 let after_rule = trimmed[4..].trim_start();
77
78 if after_rule.starts_with('"') {
80 if let Some(end_quote) = memchr::memchr(b'"', &after_rule.as_bytes()[1..]) {
81 let name = &after_rule[1..end_quote + 1];
82 let consumed = skip + 4 + (trimmed[4..].len() - after_rule.len()) + end_quote + 2;
83 return Some(RuleHeader {
84 name,
85 full_text: &text[..consumed],
86 consumed,
87 });
88 }
89 }
90
91 let name_end = after_rule
93 .find(|c: char| !c.is_alphanumeric() && c != '_')
94 .unwrap_or(after_rule.len());
95
96 if name_end > 0 {
97 let name = &after_rule[..name_end];
98 let consumed = skip + 4 + (trimmed[4..].len() - after_rule.len()) + name_end;
99 return Some(RuleHeader {
100 name,
101 full_text: &text[..consumed],
102 consumed,
103 });
104 }
105
106 None
107}
108
109pub fn parse_when_then_zero_copy(body: &str) -> Option<WhenThen<'_>> {
113 let trimmed = body.trim();
114
115 let when_pos = find_literal(trimmed, "when")?;
117 let after_when = trimmed[when_pos + 4..].trim_start();
118
119 let then_pos = find_then_keyword(after_when)?;
121
122 let condition = after_when[..then_pos].trim();
123 let action = after_when[then_pos + 4..].trim();
124
125 Some(WhenThen { condition, action })
126}
127
128pub fn parse_operator_zero_copy(text: &str) -> Option<Operator<'_>> {
130 let trimmed = text.trim_start();
131
132 if trimmed.len() >= 2 {
134 let op = &trimmed[..2];
135 if matches!(op, ">=" | "<=" | "==" | "!=") {
136 return Some(Operator { op, consumed: 2 });
137 }
138 }
139
140 if !trimmed.is_empty() {
142 let op = &trimmed[..1];
143 if matches!(op, ">" | "<") {
144 return Some(Operator { op, consumed: 1 });
145 }
146 }
147
148 if trimmed.starts_with("contains") {
150 return Some(Operator {
151 op: &trimmed[..8],
152 consumed: 8,
153 });
154 }
155 if trimmed.starts_with("matches") {
156 return Some(Operator {
157 op: &trimmed[..7],
158 consumed: 7,
159 });
160 }
161
162 None
163}
164
165pub fn parse_module_zero_copy(text: &str) -> Option<Module<'_>> {
167 let trimmed = text.trim_start();
168
169 if !trimmed.starts_with("defmodule") {
170 return None;
171 }
172
173 let after_defmodule = trimmed[9..].trim_start();
174
175 let name_end = after_defmodule.find(|c: char| !c.is_alphanumeric() && c != '_')?;
177
178 let name = &after_defmodule[..name_end];
179
180 if !name.chars().next()?.is_uppercase() {
182 return None;
183 }
184
185 let rest = after_defmodule[name_end..].trim_start();
187 if !rest.starts_with('{') {
188 return None;
189 }
190
191 let brace_pos = trimmed.len() - rest.len();
192
193 let close_pos = find_matching_brace(trimmed, brace_pos)?;
195
196 let body = &trimmed[brace_pos + 1..close_pos];
197 let consumed = close_pos + 1;
198
199 Some(Module {
200 name,
201 body,
202 consumed,
203 })
204}
205
206pub fn split_into_rules_zero_copy(grl_text: &str) -> Vec<Rule<'_>> {
210 let bytes = grl_text.as_bytes();
211 let mut rules = Vec::new();
212 let mut pos = 0;
213
214 while pos < bytes.len() {
215 if let Some(offset) = memchr::memmem::find(&bytes[pos..], b"rule ") {
217 let rule_pos = pos + offset;
218
219 if let Some(brace_offset) = memchr::memchr(b'{', &bytes[rule_pos..]) {
221 let brace_pos = rule_pos + brace_offset;
222
223 if let Some(close_pos) = find_matching_brace(grl_text, brace_pos) {
225 rules.push(Rule {
226 text: &grl_text[rule_pos..=close_pos],
227 start: rule_pos,
228 end: close_pos,
229 });
230 pos = close_pos + 1;
231 continue;
232 }
233 }
234 }
235 pos += 1;
236 }
237
238 rules
239}
240
241pub fn extract_salience_zero_copy(attributes: &str) -> Option<i32> {
243 let salience_pos = find_literal(attributes, "salience")?;
244 let after_salience = attributes[salience_pos + 8..].trim_start();
245
246 let end = after_salience
248 .find(|c: char| !c.is_ascii_digit())
249 .unwrap_or(after_salience.len());
250
251 after_salience[..end].parse().ok()
252}
253
254pub fn has_attribute_zero_copy(text: &str, attr: &str) -> bool {
256 let bytes = text.as_bytes();
257 let attr_bytes = attr.as_bytes();
258
259 if let Some(pos) = memchr::memmem::find(bytes, attr_bytes) {
260 let before_ok = pos == 0 || !bytes[pos - 1].is_ascii_alphanumeric();
262 let after_pos = pos + attr_bytes.len();
263 let after_ok = after_pos >= bytes.len() || !bytes[after_pos].is_ascii_alphanumeric();
264
265 return before_ok && after_ok;
266 }
267
268 false
269}
270
271fn find_literal(text: &str, pattern: &str) -> Option<usize> {
274 text.find(pattern)
275}
276
277fn find_then_keyword(text: &str) -> Option<usize> {
278 let bytes = text.as_bytes();
279 let mut in_string = false;
280 let mut escape_next = false;
281 let mut paren_depth = 0;
282 let mut brace_depth = 0;
283
284 let mut i = 0;
285 while i < bytes.len() {
286 if escape_next {
287 escape_next = false;
288 i += 1;
289 continue;
290 }
291
292 match bytes[i] {
293 b'\\' if in_string => escape_next = true,
294 b'"' => in_string = !in_string,
295 b'(' if !in_string => paren_depth += 1,
296 b')' if !in_string => paren_depth -= 1,
297 b'{' if !in_string => brace_depth += 1,
298 b'}' if !in_string => brace_depth -= 1,
299 b't' if !in_string && paren_depth == 0 && brace_depth == 0 => {
300 if i + 4 <= bytes.len() && &bytes[i..i + 4] == b"then" {
301 let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric();
302 let after_ok = i + 4 >= bytes.len() || !bytes[i + 4].is_ascii_alphanumeric();
303 if before_ok && after_ok {
304 return Some(i);
305 }
306 }
307 }
308 _ => {}
309 }
310 i += 1;
311 }
312
313 None
314}
315
316fn find_matching_brace(text: &str, open_pos: usize) -> Option<usize> {
317 let bytes = text.as_bytes();
318
319 if open_pos >= bytes.len() || bytes[open_pos] != b'{' {
320 return None;
321 }
322
323 let mut depth = 1;
324 let mut in_string = false;
325 let mut escape_next = false;
326
327 #[allow(clippy::needless_range_loop)]
328 for i in (open_pos + 1)..bytes.len() {
329 if escape_next {
330 escape_next = false;
331 continue;
332 }
333
334 match bytes[i] {
335 b'\\' if in_string => escape_next = true,
336 b'"' => in_string = !in_string,
337 b'{' if !in_string => depth += 1,
338 b'}' if !in_string => {
339 depth -= 1;
340 if depth == 0 {
341 return Some(i);
342 }
343 }
344 _ => {}
345 }
346 }
347
348 None
349}
350
351impl<'a> fmt::Display for RuleHeader<'a> {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 write!(f, "rule \"{}\"", self.name)
356 }
357}
358
359impl<'a> fmt::Display for WhenThen<'a> {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 write!(f, "when {} then {}", self.condition, self.action)
362 }
363}
364
365impl<'a> fmt::Display for Module<'a> {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 write!(f, "defmodule {} {{ {} }}", self.name, self.body)
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn test_parse_rule_header_zero_copy() {
377 let text = r#"rule "MyRule" {"#;
378 let header = parse_rule_header_zero_copy(text).unwrap();
379 assert_eq!(header.name, "MyRule");
380 assert!(header.consumed > 0);
381 }
382
383 #[test]
384 fn test_parse_when_then_zero_copy() {
385 let body = "when X > 5 then Y = 10";
386 let wt = parse_when_then_zero_copy(body).unwrap();
387 assert_eq!(wt.condition, "X > 5");
388 assert_eq!(wt.action, "Y = 10");
389 }
390
391 #[test]
392 fn test_parse_operator_zero_copy() {
393 let op = parse_operator_zero_copy(">=").unwrap();
394 assert_eq!(op.op, ">=");
395 assert_eq!(op.consumed, 2);
396
397 let op2 = parse_operator_zero_copy("contains").unwrap();
398 assert_eq!(op2.op, "contains");
399 assert_eq!(op2.consumed, 8);
400 }
401
402 #[test]
403 fn test_parse_module_zero_copy() {
404 let text = "defmodule MYMODULE { export: all }";
405 let module = parse_module_zero_copy(text).unwrap();
406 assert_eq!(module.name, "MYMODULE");
407 assert!(module.body.contains("export"));
408 }
409
410 #[test]
411 fn test_split_into_rules_zero_copy() {
412 let grl = r#"
413rule "Rule1" { when X > 5 then Y = 10 }
414rule "Rule2" { when A < 3 then B = 7 }
415 "#;
416 let rules = split_into_rules_zero_copy(grl);
417 assert_eq!(rules.len(), 2);
418 assert!(rules[0].text.contains("Rule1"));
419 assert!(rules[1].text.contains("Rule2"));
420 }
421
422 #[test]
423 fn test_extract_salience_zero_copy() {
424 assert_eq!(extract_salience_zero_copy("salience 10"), Some(10));
425 assert_eq!(extract_salience_zero_copy("salience 42 "), Some(42));
426 }
427
428 #[test]
429 fn test_has_attribute_zero_copy() {
430 assert!(has_attribute_zero_copy("no-loop lock-on-active", "no-loop"));
431 assert!(!has_attribute_zero_copy("no-loops", "no-loop"));
432 }
433
434 #[test]
435 fn test_zero_allocations() {
436 let text = r#"rule "TestRule" { when X > 5 then Y = 10 }"#;
438
439 let header = parse_rule_header_zero_copy(text).unwrap();
440
441 let text_start = text.as_ptr() as usize;
443 let text_end = unsafe { text.as_ptr().add(text.len()) as usize };
444 let name_ptr = header.name.as_ptr() as usize;
445
446 assert!(name_ptr >= text_start);
447 assert!(name_ptr < text_end);
448 }
449}