Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The rule language

rust-runes reads GRL, the rule language of Grule, following Grule’s grammar (grulev3.g4).

Rules

rule Name "optional description" salience 10 {
    when <condition>
    then <statement>; <statement>; ...
}
  • Name: unique across the engine; Retract and errors refer to rules by name.
  • Description: optional, in quotes.
  • Salience: optional priority, an integer (negative too), default 0. When several rules match, the highest salience fires first; see the execution model.
  • when: an expression that must evaluate to a boolean.
  • then: statements, each ending with ;. The block may be empty.

Keywords (rule, salience, when, then, true, false, nil) are case-insensitive, and identifiers may use Unicode letters. Comments use // and /* */.

/* Discounts for loyal customers */
rule LoyaltyDiscount "Loyal customers get 10% off" salience 20 {
    when
        Customer.Years >= 5 &&      // five years or more
        !Order.Discounted
    then
        Order.Total *= 0.9;
        Order.Discounted = true;
}

Values

KindLiterals
Strings"..." or '...', with Go’s escapes: \n, \t, \", \\, \x41, \101, é, \U0001F600
Integers42, -7, 0x2A (hex), 052 (octal)
Floats1.5, .5, 1e3, 2.5E-3, 0x1p-2
Booleanstrue, false
Nilnil

Times have no literal; they come from Now(), MakeTime(...) or facts. Facts may also hold objects and arrays.

Reading facts

  • Fact reads a fact; Fact.Field.Inner reads nested fields.
  • Fact.Items[0] indexes arrays, and Fact.Codes["key"] or Fact.Codes[404] maps.
  • Fact.Method(args) calls a method, and Function(args) a function. Calls can be chained: Fact.Name.ToUpper().HasPrefix("A").

Reading a field or element that doesn’t exist is an error. In a condition, errors make the condition false by default (see failed conditions).

Operators

From lowest to highest precedence:

OperatorsNotes
||Logical or, short-circuits
&&Logical and, short-circuits
== != < <= > >=See comparisons below
+ - & |+ also joins strings; & and | are bitwise, on integers
* / %% takes integers
! -Unary not and negation
.Field [index] .Method()

Parentheses group as usual. Unlike in many languages, all comparisons share one level and & and | bind tighter than comparisons, as in Grule.

Arithmetic. Two integers give an integer and anything with a float gives a float, except /, which always gives a float: 7 / 2 == 3.5. Use Floor or assign to an integer field to truncate. Division by zero is an error.

Strings. + joins two strings, or a string with a number, boolean or time ("Total: " + 9.5 is "Total: 9.500000", formatting floats as Go’s %f does).

Comparisons. Numbers compare with numbers (integers and floats by value), strings with strings and times with times; all three can be ordered. A string or boolean is never equal to a value of another type, nil equals only nil, and comparing a number with a non-number is an error.

Booleans are strict: &&, || and ! take booleans only, and a condition must be a boolean. There is no truthiness.

These rules are all checked by tests:

use rust_runes::{Fact, FactValue, RuleEngine};
use std::collections::HashMap;
let mut engine = RuleEngine::new();
engine.add_rules_from_str(r#"
    rule Check {
        when !R.Done
        then
            R.Div = 7 / 2;
            R.Mod = 7 % 2;
            R.Mixed = 2 * 1.5;
            R.Joined = "n=" + 3 + ", ok=" + true;
            R.Precedence = 1 + 2 * 3 == 7 && 1 | 2 == 3;
            R.Done = true;
    }
"#)?;
let mut facts = HashMap::from([(
    "R".to_string(),
    Fact::from_object("R".to_string(), HashMap::from([("Done".to_string(), false.into())])),
)]);
engine.execute(&mut facts)?;

let r = &facts["R"];
assert_eq!(r.get_field("Div"), Some(&FactValue::Float(3.5)));
assert_eq!(r.get_field("Mod"), Some(&FactValue::Integer(1)));
assert_eq!(r.get_field("Mixed"), Some(&FactValue::Float(3.0)));
assert_eq!(r.get_field("Joined"), Some(&FactValue::from("n=3, ok=true")));
assert_eq!(r.get_field("Precedence"), Some(&FactValue::Boolean(true)));
Ok::<(), Box<dyn std::error::Error>>(())

Statements

The then block holds statements:

  • Assignments to facts, fields and elements: =, +=, -=, *=, /=. Order.Lines[0].Price *= 2; changes the nested element in place.
  • Function calls: Log("checked");, Retract("ThisRule");, Complete();.
  • Method calls: Order.Items.Append("gift");. A method called as a statement on a fact path changes the stored fact.

Assignments keep the type of a field that already has a value; see Facts.

rule Restock "Reorder low stock" salience 5 {
    when
        Item.Stock < Item.ReorderLevel && !Item.Ordered
    then
        Item.Order.Quantity = Item.ReorderLevel * 2 - Item.Stock;
        Item.History.Append("reordered " + Item.Order.Quantity);
        Item.Ordered = true;
        Log("Reordering " + Item.Name);
}