-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Documentation on the rule engine #149
Comments
Idea from the meeting ( from @lixitrixi @gskorokhod): We currently do: constraints = Vec<Expression> Why don't we do constraints: Expression and use the |
Nik and Felix: |
The register_rule macro should also be opaque - move its documentation into the actual file (it's also an implementation detail) |
Question to consider: what is our process for testing the rules? |
Do integration tests currently do code coverage? (they should) |
Integration tests are probably a way to test rules without writing separate tests for all of them |
My thought is that the essence file integration tests + coverage would be enough? (a very important and good question though. @ozgurakgun ) |
agreed. it's very hard to test individual rules. and integration testing should contribute to the coverage stats. |
Hi!
I have written some basic documentation on how we're planning to handle rules and rule application to help myself and other people who are just getting into the Rust side of the project and want to work on the rule engine.
Please comment any corrections / anything that I've missed in this issue!
Overview
Our starting point is the AST for the Essence code, in JSON format. It is taken from the output of
conjure pretty
.This AST is parsed into a
Model
struct. The parsing logic is contained inconjure_oxide/src/parse.rs
.The
Model
struct contains:variables
)constraints
) as aVector<Expression>
The
Expression
type is an enum that represents an expression treeThe original expression trees are rewritten into a format that the different solvers can "understand" (e.g. boolean expressions in CNF for SAT solvers). We use a rule engine for that.
Conceptually,
Rule
structs have anapplication
method that takes an expression and either produces a rewritten expression (if the rule is applicable) or an error (if it is not applicable)To rewrite the expressions, we walk the expression tree, find rules that can be applied for each expression, and apply them until there are no more rules to apply.
Where multiple rules are applicable, we want:
When a rule is applied to an
Expression
in the expression tree, this expression is rewritten and all its ancestor nodes are marked as "dirty" (i.e. we need to check them again to make sure that no new rules have appeared that can be applied)Rules are stored in the
RULES_DISTRIBUTED_SLICE
object.The
#[register_rule]
macro is used to register rules from a Rust function. We use it to define rules and add them to the rule table.We call the
rewrite
function on ourModel
to recursively apply rules to expressions to simplify them, until there are no more rules to applyAfter the rewrite is complete, we can convert the resulting model to the specific representation that is required by the solver (e.g. CNF for KissSAT)
The rewrite function
The
rewrite
function takes a model, gets rules from theRULES_DISTRIBUTED_SLICE
, and recursively applies rules to the expression tree until no more rules can be appliedAlgorithm
Rule::application()
function errors, the rule is not applicableRule::application()
select
function to choose the rule to apply (this may be based on rule priority and other metadata)Expression
and traverse the tree again (step 1)Optimisation
Expression
in the expression tree, mark it as "clean"This works because if we have already found that no rules can be applied to an expression, this will still be the case for as long as this expression doesn't change (so we don't need to retry this check multiple times).
However, if its sub-expressions have been changed, we need to check whether any new rules can now be applied to the expression (and all its parents)
The Rule struct
The
Rule
struct represents a single rule in the rule engineFields
name
- a human-readable name for the ruleapplication
- a function that tries to apply the rule to a given expression, and either returns the rewritten expression or errorsNote
NB: the logic for checking whether a rule is applicable and actually applying it is the same, so, for efficiency reasons, we do not have separate methods for these tasks. Instead, we try to apply the rule right away, and save the result if the rule turns out to be applicable to the given expression.
The register_rule macro
The
#[register_rule]
macro is a procedural macro that decorates a Rust function, constructs a Rule struct from it, and adds the struct to the centralised rule table (RULES_DISTRIBUTE_SLICE
)Explanation
Essentially, the macro:
Rule
struct and stores it inGEN_CONJURE_<function identifier>
application
fieldname
fieldExample
Would produce:
And append it to
RULES_DISTRIBUTED_SLICE
The centralised rule table (RULES_DISTRIBUTED_SLICE)
RULES_DISTRIBUTED_SLICE
represents our rule table that contains all the Rule structs that have been defined across our project.Explanation
RULES_DISTRIBUTED_SLICE
is a Rust object that represents a region in memory, similar to an array.#[register_rule]
macro.The text was updated successfully, but these errors were encountered: