Skip to main content

rust_runes/
parser.rs

1use crate::ast::Expression;
2use crate::rule::Rule;
3use pest::iterators::{Pair, Pairs};
4use pest::pratt_parser::{Assoc, Op, PrattParser};
5use pest::Parser;
6
7// Kept in its own module because the derive generates an enum named `Rule`,
8// which would clash with `crate::rule::Rule`.
9mod grammar {
10    #[derive(pest_derive::Parser)]
11    #[grammar = "grl.pest"]
12    pub struct GrlGrammar;
13}
14
15use grammar::{GrlGrammar, Rule as GrammarRule};
16
17/// Parses GRL (Grule Rule Language) text into [`Rule`]s.
18///
19/// Most code doesn't need this directly: [`RuleEngine::add_rules_from_str`](crate::RuleEngine::add_rules_from_str)
20/// and the file loaders use it.
21pub struct GrlParser {
22    pratt: PrattParser<GrammarRule>,
23}
24
25impl GrlParser {
26    /// A parser.
27    pub fn new() -> Self {
28        // Lowest precedence first, matching Grule: all comparisons share one level,
29        // and `&` / `|` bind like `+` / `-`
30        let pratt = PrattParser::new()
31            .op(Op::infix(GrammarRule::or, Assoc::Left))
32            .op(Op::infix(GrammarRule::and, Assoc::Left))
33            .op(Op::infix(GrammarRule::eq, Assoc::Left)
34                | Op::infix(GrammarRule::ne, Assoc::Left)
35                | Op::infix(GrammarRule::lt, Assoc::Left)
36                | Op::infix(GrammarRule::le, Assoc::Left)
37                | Op::infix(GrammarRule::gt, Assoc::Left)
38                | Op::infix(GrammarRule::ge, Assoc::Left))
39            .op(Op::infix(GrammarRule::add, Assoc::Left)
40                | Op::infix(GrammarRule::sub, Assoc::Left)
41                | Op::infix(GrammarRule::bitand, Assoc::Left)
42                | Op::infix(GrammarRule::bitor, Assoc::Left))
43            .op(Op::infix(GrammarRule::mul, Assoc::Left)
44                | Op::infix(GrammarRule::div, Assoc::Left)
45                | Op::infix(GrammarRule::modulo, Assoc::Left))
46            .op(Op::prefix(GrammarRule::not) | Op::prefix(GrammarRule::neg))
47            .op(Op::postfix(GrammarRule::method)
48                | Op::postfix(GrammarRule::field)
49                | Op::postfix(GrammarRule::index));
50
51        Self { pratt }
52    }
53
54    /// Parses text containing exactly one rule.
55    pub fn parse_rule(&self, grl_text: &str) -> std::result::Result<Rule, String> {
56        let mut rules = self.parse_rules(grl_text)?;
57        match rules.len() {
58            1 => Ok(rules.remove(0)),
59            n => Err(format!("Expected exactly one rule, found {}", n)),
60        }
61    }
62
63    /// Parses text containing any number of rules.
64    pub fn parse_rules(&self, grl_text: &str) -> std::result::Result<Vec<Rule>, String> {
65        let grl = GrlGrammar::parse(GrammarRule::grl, grl_text)
66            .map_err(|e| format!("Invalid GRL syntax:\n{}", e))?
67            .next()
68            .unwrap();
69
70        grl.into_inner()
71            .filter(|pair| pair.as_rule() == GrammarRule::rule_def)
72            .map(|pair| self.parse_rule_def(pair))
73            .collect()
74    }
75
76    fn parse_rule_def(&self, pair: Pair<GrammarRule>) -> std::result::Result<Rule, String> {
77        let mut name = String::new();
78        let mut description = None;
79        let mut salience = 0;
80        let mut when_condition = None;
81        let mut then_actions = Vec::new();
82
83        for part in pair.into_inner() {
84            match part.as_rule() {
85                GrammarRule::ident => name = part.as_str().to_string(),
86                GrammarRule::description => {
87                    description = Some(parse_string(part.into_inner().next().unwrap())?)
88                }
89                GrammarRule::salience => {
90                    let value = part.into_inner().next_back().unwrap();
91                    salience = parse_integer(value.as_str())
92                        .and_then(|n| i32::try_from(n).map_err(|e| e.to_string()))
93                        .map_err(|e| format!("Invalid salience {}: {}", value.as_str(), e))?;
94                }
95                GrammarRule::expr => when_condition = Some(self.parse_expr(part.into_inner())?),
96                GrammarRule::action => then_actions.push(self.parse_action(part)?),
97                _ => {} // keywords
98            }
99        }
100
101        let mut rule = Rule::new(name, salience, when_condition.unwrap(), then_actions);
102        if let Some(desc) = description {
103            rule = rule.with_description(desc);
104        }
105        Ok(rule)
106    }
107
108    fn parse_action(&self, pair: Pair<GrammarRule>) -> std::result::Result<Expression, String> {
109        let (line, col) = pair.line_col();
110        let mut parts = pair.into_inner();
111        let target_pair = parts.next().unwrap();
112        let target_text = target_pair.as_str().trim_end().to_string();
113        let target = self.parse_expr(target_pair.into_inner())?;
114
115        let Some(op) = parts.next() else {
116            return match target {
117                Expression::Call(..) | Expression::MethodCall(..) => Ok(target),
118                _ => Err(format!(
119                    "{}:{}: expected an assignment or function call, found `{}`",
120                    line, col, target_text
121                )),
122            };
123        };
124        if !target.is_assignable() {
125            return Err(format!(
126                "{}:{}: cannot assign to `{}`",
127                line, col, target_text
128            ));
129        }
130
131        let mut value = self.parse_expr(parts.next().unwrap().into_inner())?;
132
133        // Desugar compound assignment: `x += e` becomes `x = x + e`
134        if op.as_str() != "=" {
135            let current = Box::new(target.clone());
136            let rhs = Box::new(value);
137            value = match op.as_str() {
138                "+=" => Expression::Add(current, rhs),
139                "-=" => Expression::Subtract(current, rhs),
140                "*=" => Expression::Multiply(current, rhs),
141                "/=" => Expression::Divide(current, rhs),
142                other => unreachable!("unknown assignment operator {}", other),
143            };
144        }
145
146        Ok(Expression::Assign(Box::new(target), Box::new(value)))
147    }
148
149    fn parse_args(&self, pair: Pair<GrammarRule>) -> std::result::Result<Vec<Expression>, String> {
150        pair.into_inner()
151            .map(|arg| self.parse_expr(arg.into_inner()))
152            .collect()
153    }
154
155    fn parse_expr(&self, pairs: Pairs<GrammarRule>) -> std::result::Result<Expression, String> {
156        self.pratt
157            .map_primary(|primary| match primary.as_rule() {
158                GrammarRule::integer => parse_integer(primary.as_str())
159                    .map(Expression::Integer)
160                    .map_err(|e| format!("Invalid integer {}: {}", primary.as_str(), e)),
161                GrammarRule::float => parse_float(primary.as_str())
162                    .map(Expression::Float)
163                    .map_err(|e| format!("Invalid number {}: {}", primary.as_str(), e)),
164                GrammarRule::string => parse_string(primary).map(Expression::String),
165                GrammarRule::boolean => Ok(Expression::Boolean(
166                    primary.as_str().eq_ignore_ascii_case("true"),
167                )),
168                GrammarRule::nil => Ok(Expression::Nil),
169                GrammarRule::ident => Ok(Expression::Variable(primary.as_str().to_string())),
170                GrammarRule::call => {
171                    let mut inner = primary.into_inner();
172                    let name = inner.next().unwrap().as_str().to_string();
173                    Ok(Expression::Call(
174                        name,
175                        self.parse_args(inner.next().unwrap())?,
176                    ))
177                }
178                GrammarRule::expr => self.parse_expr(primary.into_inner()),
179                rule => unreachable!("unexpected primary {:?}", rule),
180            })
181            .map_prefix(|op, operand| {
182                let operand = operand?;
183                Ok(match op.as_rule() {
184                    GrammarRule::not => Expression::Not(Box::new(operand)),
185                    GrammarRule::neg => match operand {
186                        Expression::Integer(n) => Expression::Integer(n.wrapping_neg()),
187                        Expression::Float(n) => Expression::Float(-n),
188                        other => {
189                            Expression::Subtract(Box::new(Expression::Integer(0)), Box::new(other))
190                        }
191                    },
192                    rule => unreachable!("unexpected prefix {:?}", rule),
193                })
194            })
195            .map_infix(|lhs, op, rhs| {
196                let (lhs, rhs) = (Box::new(lhs?), Box::new(rhs?));
197                Ok(match op.as_rule() {
198                    GrammarRule::or => Expression::Or(lhs, rhs),
199                    GrammarRule::and => Expression::And(lhs, rhs),
200                    GrammarRule::eq => Expression::Equal(lhs, rhs),
201                    GrammarRule::ne => Expression::NotEqual(lhs, rhs),
202                    GrammarRule::lt => Expression::LessThan(lhs, rhs),
203                    GrammarRule::le => Expression::LessEqual(lhs, rhs),
204                    GrammarRule::gt => Expression::GreaterThan(lhs, rhs),
205                    GrammarRule::ge => Expression::GreaterEqual(lhs, rhs),
206                    GrammarRule::add => Expression::Add(lhs, rhs),
207                    GrammarRule::sub => Expression::Subtract(lhs, rhs),
208                    GrammarRule::mul => Expression::Multiply(lhs, rhs),
209                    GrammarRule::div => Expression::Divide(lhs, rhs),
210                    GrammarRule::modulo => Expression::Modulo(lhs, rhs),
211                    GrammarRule::bitand => Expression::BitAnd(lhs, rhs),
212                    GrammarRule::bitor => Expression::BitOr(lhs, rhs),
213                    rule => unreachable!("unexpected infix {:?}", rule),
214                })
215            })
216            .map_postfix(|lhs, op| {
217                let lhs = Box::new(lhs?);
218                let mut inner = op.clone().into_inner();
219                Ok(match op.as_rule() {
220                    GrammarRule::field => {
221                        Expression::FieldAccess(lhs, inner.next().unwrap().as_str().to_string())
222                    }
223                    GrammarRule::index => Expression::Index(
224                        lhs,
225                        Box::new(self.parse_expr(inner.next().unwrap().into_inner())?),
226                    ),
227                    GrammarRule::method => {
228                        let name = inner.next().unwrap().as_str().to_string();
229                        Expression::MethodCall(lhs, name, self.parse_args(inner.next().unwrap())?)
230                    }
231                    rule => unreachable!("unexpected postfix {:?}", rule),
232                })
233            })
234            .parse(pairs)
235    }
236}
237
238fn parse_string(pair: Pair<GrammarRule>) -> std::result::Result<String, String> {
239    // string -> dq_string | sq_string -> dq_inner | sq_inner
240    let inner = pair
241        .into_inner()
242        .next()
243        .unwrap()
244        .into_inner()
245        .next()
246        .unwrap();
247    let quote = if inner.as_rule() == GrammarRule::dq_inner {
248        '"'
249    } else {
250        '\''
251    };
252    unescape(inner.as_str(), quote)
253}
254
255/// Decodes escapes as Go's `strconv.UnquoteChar` does (which Grule uses): `\a \b \f \n
256/// \r \t \v \\ \' \"`, `\xNN` and three-digit octal `\NNN` (raw bytes), and `\uNNNN` /
257/// `\UNNNNNNNN` (Unicode code points). A doubled quote also stands for one quote.
258fn unescape(raw: &str, quote: char) -> std::result::Result<String, String> {
259    let mut out: Vec<u8> = Vec::with_capacity(raw.len());
260    let mut chars = raw.chars().peekable();
261    let digits = |chars: &mut std::iter::Peekable<std::str::Chars>, count: usize, radix: u32| {
262        let text: String = chars.by_ref().take(count).collect();
263        if text.len() != count {
264            return Err(format!("Incomplete escape sequence: {}", text));
265        }
266        u32::from_str_radix(&text, radix).map_err(|_| format!("Invalid escape sequence: {}", text))
267    };
268    while let Some(c) = chars.next() {
269        if c == quote {
270            // A doubled quote stands for one quote character
271            chars.next();
272            out.push(quote as u8);
273            continue;
274        }
275        if c != '\\' {
276            out.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes());
277            continue;
278        }
279        let Some(escape) = chars.next() else {
280            return Err("Unterminated escape sequence".to_string());
281        };
282        let byte = match escape {
283            'a' => 0x07,
284            'b' => 0x08,
285            'f' => 0x0c,
286            'n' => b'\n',
287            'r' => b'\r',
288            't' => b'\t',
289            'v' => 0x0b,
290            '\\' => b'\\',
291            '\'' => b'\'',
292            '"' => b'"',
293            'x' => digits(&mut chars, 2, 16)? as u8,
294            '0'..='7' => {
295                let rest = digits(&mut chars, 2, 8)?;
296                let value = escape.to_digit(8).unwrap() * 64 + rest;
297                u8::try_from(value).map_err(|_| format!("Octal escape out of range: {}", value))?
298            }
299            'u' | 'U' => {
300                let code = digits(&mut chars, if escape == 'u' { 4 } else { 8 }, 16)?;
301                let c = char::from_u32(code)
302                    .ok_or_else(|| format!("Invalid Unicode code point: {:X}", code))?;
303                out.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes());
304                continue;
305            }
306            other => return Err(format!("Invalid escape sequence: \\{}", other)),
307        };
308        out.push(byte);
309    }
310    String::from_utf8(out).map_err(|_| "String literal is not valid UTF-8".to_string())
311}
312
313/// Parses a decimal, hexadecimal (`0x1F`) or octal (`017`) integer literal,
314/// optionally preceded by `-`.
315fn parse_integer(text: &str) -> std::result::Result<i64, String> {
316    let (negative, digits) = match text.strip_prefix('-') {
317        Some(rest) => (true, rest),
318        None => (false, text),
319    };
320    let magnitude = if let Some(hex) = digits
321        .strip_prefix("0x")
322        .or_else(|| digits.strip_prefix("0X"))
323    {
324        i64::from_str_radix(hex, 16)
325    } else if digits.len() > 1 && digits.starts_with('0') {
326        i64::from_str_radix(&digits[1..], 8)
327    } else {
328        digits.parse()
329    }
330    .map_err(|e| e.to_string())?;
331    Ok(if negative { -magnitude } else { magnitude })
332}
333
334/// Parses a decimal float (`1.5`, `2e10`, `.5`) or hexadecimal float (`0x1.8p3`) literal.
335fn parse_float(text: &str) -> std::result::Result<f64, String> {
336    let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) else {
337        return text
338            .parse()
339            .map_err(|e: std::num::ParseFloatError| e.to_string());
340    };
341    let (mantissa, exponent) = hex.split_once(['p', 'P']).ok_or("missing exponent")?;
342    let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
343    let mut value = 0.0;
344    for digit in whole.chars().chain(fraction.chars()) {
345        value = value * 16.0 + digit.to_digit(16).ok_or("invalid hex digit")? as f64;
346    }
347    let exponent: i32 = exponent
348        .parse()
349        .map_err(|e: std::num::ParseIntError| e.to_string())?;
350    Ok(value * 2f64.powi(exponent - 4 * fraction.len() as i32))
351}
352
353impl Default for GrlParser {
354    fn default() -> Self {
355        Self::new()
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362    use crate::engine::RuleEngine;
363    use crate::facts::{Fact, FactValue};
364    use std::collections::HashMap;
365
366    fn var(name: &str) -> Box<Expression> {
367        Box::new(Expression::Variable(name.to_string()))
368    }
369
370    fn num(n: i64) -> Box<Expression> {
371        Box::new(Expression::Integer(n))
372    }
373
374    fn when_of(condition: &str) -> Expression {
375        let grl = format!("rule R {{ when {} then }}", condition);
376        GrlParser::new().parse_rule(&grl).unwrap().when_condition
377    }
378
379    #[test]
380    fn test_arithmetic_precedence() {
381        // a + b * c parses as a + (b * c)
382        assert_eq!(
383            when_of("a + b * c > 0"),
384            Expression::GreaterThan(
385                Box::new(Expression::Add(
386                    var("a"),
387                    Box::new(Expression::Multiply(var("b"), var("c")))
388                )),
389                num(0),
390            )
391        );
392    }
393
394    #[test]
395    fn test_logical_precedence() {
396        // && binds tighter than ||
397        assert_eq!(
398            when_of("a || b && c"),
399            Expression::Or(var("a"), Box::new(Expression::And(var("b"), var("c"))))
400        );
401    }
402
403    #[test]
404    fn test_parentheses_override_precedence() {
405        assert_eq!(
406            when_of("(a || b) && c"),
407            Expression::And(Box::new(Expression::Or(var("a"), var("b"))), var("c"))
408        );
409        assert_eq!(
410            when_of("(a + b) * c == 1"),
411            Expression::Equal(
412                Box::new(Expression::Multiply(
413                    Box::new(Expression::Add(var("a"), var("b"))),
414                    var("c")
415                )),
416                num(1),
417            )
418        );
419    }
420
421    #[test]
422    fn test_left_associativity() {
423        // a - b - c parses as (a - b) - c
424        assert_eq!(
425            when_of("a - b - c == 0"),
426            Expression::Equal(
427                Box::new(Expression::Subtract(
428                    Box::new(Expression::Subtract(var("a"), var("b"))),
429                    var("c")
430                )),
431                num(0),
432            )
433        );
434    }
435
436    #[test]
437    fn test_unary_operators() {
438        assert_eq!(
439            when_of("!done && x > -5"),
440            Expression::And(
441                Box::new(Expression::Not(var("done"))),
442                Box::new(Expression::GreaterThan(var("x"), num(-5))),
443            )
444        );
445        assert_eq!(
446            when_of("-x < 0"),
447            Expression::LessThan(Box::new(Expression::Subtract(num(0), var("x"))), num(0))
448        );
449    }
450
451    #[test]
452    fn test_nested_field_access() {
453        assert_eq!(
454            when_of("order.customer.age >= 18"),
455            Expression::GreaterEqual(
456                Box::new(Expression::FieldAccess(
457                    Box::new(Expression::FieldAccess(
458                        var("order"),
459                        "customer".to_string()
460                    )),
461                    "age".to_string(),
462                )),
463                num(18),
464            )
465        );
466    }
467
468    #[test]
469    fn test_string_escapes() {
470        assert_eq!(
471            when_of(r#"msg == "say \"hi\"\n""#),
472            Expression::Equal(
473                var("msg"),
474                Box::new(Expression::String("say \"hi\"\n".to_string()))
475            )
476        );
477    }
478
479    #[test]
480    fn test_identifiers_starting_with_keywords() {
481        assert_eq!(when_of("ruleset && thenable && trueish"), {
482            Expression::And(
483                Box::new(Expression::And(var("ruleset"), var("thenable"))),
484                var("trueish"),
485            )
486        });
487    }
488
489    #[test]
490    fn test_compound_assignment() {
491        let rule = GrlParser::new()
492            .parse_rule("rule R { when true then x += 1; car.Speed *= 2; }")
493            .unwrap();
494        assert_eq!(
495            rule.then_actions,
496            vec![
497                Expression::assign(&["x"], Expression::Add(var("x"), num(1))),
498                Expression::assign(
499                    &["car", "Speed"],
500                    Expression::Multiply(
501                        Box::new(Expression::FieldAccess(var("car"), "Speed".to_string())),
502                        num(2),
503                    ),
504                ),
505            ]
506        );
507    }
508
509    #[test]
510    fn test_multiple_rules_with_comments() {
511        let grl = r#"
512            // Line comment
513            rule First "first rule" salience 10 {
514                when x > 1 /* inline comment */ then y = 1;
515            }
516
517            /* Block
518               comment */
519            rule Second salience -5 {
520                when true
521                then z = "two";
522            }
523        "#;
524        let rules = GrlParser::new().parse_rules(grl).unwrap();
525        assert_eq!(rules.len(), 2);
526        assert_eq!(rules[0].name, "First");
527        assert_eq!(rules[0].description, Some("first rule".to_string()));
528        assert_eq!(rules[0].salience, 10);
529        assert_eq!(rules[1].name, "Second");
530        assert_eq!(rules[1].description, None);
531        assert_eq!(rules[1].salience, -5);
532    }
533
534    #[test]
535    fn test_parse_rule_requires_exactly_one_rule() {
536        let parser = GrlParser::new();
537        assert!(parser.parse_rule("").is_err());
538        assert!(parser
539            .parse_rule("rule A { when true then } rule B { when true then }")
540            .is_err());
541    }
542
543    #[test]
544    fn test_syntax_errors_report_location() {
545        let parser = GrlParser::new();
546        let err = parser
547            .parse_rule("rule Broken {\n  when x > \n  then y = 1;\n}")
548            .unwrap_err();
549        assert!(err.contains("3:"), "error should point at line 3: {}", err);
550
551        assert!(parser
552            .parse_rule("rule R { when x > 1 then y = 1 }")
553            .is_err()); // missing ';'
554        assert!(parser
555            .parse_rule("rule R { when (x > 1 then y = 1; }")
556            .is_err()); // unbalanced paren
557        assert!(parser.parse_rule("rule then { when true then }").is_err()); // keyword as name
558    }
559
560    #[test]
561    fn test_nested_field_assignment() {
562        let rule = GrlParser::new()
563            .parse_rule("rule R { when true then a.b.c = 1; a.b.c += 2; }")
564            .unwrap();
565        let current = Expression::FieldAccess(
566            Box::new(Expression::FieldAccess(var("a"), "b".to_string())),
567            "c".to_string(),
568        );
569        assert_eq!(
570            rule.then_actions,
571            vec![
572                Expression::assign(&["a", "b", "c"], Expression::Integer(1)),
573                Expression::assign(&["a", "b", "c"], Expression::Add(Box::new(current), num(2))),
574            ]
575        );
576    }
577
578    #[test]
579    fn test_parsed_rule_executes() {
580        let grl = r#"
581            rule CheckEligibility "Check if customer is eligible" salience 10 {
582                when
583                    customer.age >= 18 && (customer.balance > 1000 || customer.vip == true)
584                then
585                    customer.eligible = true;
586                    message = "Customer is eligible for premium service";
587                    Retract("CheckEligibility");
588            }
589        "#;
590        let mut engine = RuleEngine::new();
591        engine
592            .add_rule(GrlParser::new().parse_rule(grl).unwrap())
593            .unwrap();
594
595        let mut facts = HashMap::new();
596        facts.insert(
597            "customer".to_string(),
598            Fact::from_object(
599                "customer".to_string(),
600                HashMap::from([
601                    ("age".to_string(), FactValue::Integer(30)),
602                    ("balance".to_string(), FactValue::Integer(500)),
603                    ("vip".to_string(), FactValue::Boolean(true)),
604                ]),
605            ),
606        );
607
608        let result = engine.execute(&mut facts).unwrap();
609        assert_eq!(result.rules_fired, vec!["CheckEligibility".to_string()]);
610        assert_eq!(
611            facts["customer"].get_field("eligible"),
612            Some(&FactValue::Boolean(true))
613        );
614        assert_eq!(
615            facts["message"].value,
616            FactValue::String("Customer is eligible for premium service".to_string())
617        );
618    }
619}