|
ContentsConstraint Handling RulesSCL embeds a CHR (Constraint Handling Rules) engine: a forward-chaining rule system that works on a constraint store, a bag of facts. You declare the shapes of facts, write rules that match facts and produce new ones, and the engine runs the rules to exhaustion. It is the right tool for fixpoint computations, graph traversal, and anything naturally expressed as "whenever these facts are present, do this". This is an advanced feature. Nothing here is needed for ordinary SCL programming. A first example
Read the three rules as:
The engine runs until no rule can fire. Rule 2 keeps collapsing pairs until one Where rules may be writtenA CHR rule is a statement, so rules live in a block:
All the rules in one block share one constraint store, and the store lives exactly as long as one evaluation of that block. Two blocks never share constraints, even if they declare the same names:
For a store that outlives a single call, see Rulesets. No module header is required. ConstraintsA
Constraints may be polymorphic in the block's type variables:
Declaring a constraint is optional. An undeclared name used in a rule is inferred as a new constraint of that block, with argument types taken from its uses:
Declaring them is still worth it: a declaration fixes the arity and types, so a typo becomes Constraint is applied with wrong number of parameters instead of a second, silently unrelated constraint. Record constraintsA constraint can name its arguments instead of taking them positionally:
Every field must be given when the fact is created — a missing one is Field y not defined. In a rule head you may mention only the fields you need. Record syntax requires a declaration; using it on an inferred constraint is Relation must be declared if record syntax is used, and using it on a positional one is Relation V does not define field names. A constraint may also carry an ordinary record value, in which case the pattern applies to that value:
RulesThe basic form is
where the head is a comma-separated conjunction of literals and the body is a comma-separated sequence of constraints to add and expressions to evaluate. The rule fires once for every combination of facts matching the head. Head literals
A rule that removes its inputs makes progress by construction:
Variables
A
New existential variables may appear only in a head. A BodiesA body may add constraints and evaluate effectful expressions, in any mix:
Bodies run for their effects, so a rule body typically writes to a
|
| Name | Type | Meaning |
|---|---|---|
Name |
a type | the store's type |
createName |
() -> <Proc> Name |
creates an empty store |
createName takes no argument — write s = createIntegerSet, not createIntegerSet ().
A block joins an existing store with the include statement, after which it can match
and add that ruleset's constraints:
addTo :: IntegerSet -> Integer -> <Proc> ()
addTo set e = ()
where
include IntegerSet set
True => Element e
dump :: IntegerSet -> <Proc> ()
dump set = ()
where
include IntegerSet set
Element ?x => print "have \(?x)"
main = ()
where
s = createIntegerSet
addTo s 1
addTo s 2
addTo s 1 // prints "duplicate 1"
dump s // prints "have 2", "have 1"
The ruleset's own rules — here the duplicate check — run whenever any block that includes it adds a matching fact, not only inside the declaring module. A block may add rules of its own on top of the included ones.
Only the ruleset declaration needs features = [chr]. A module that merely imports a
ruleset and uses include, createName and the constraints needs no header at all.
With features = [chr], select is compiled by the CHR engine instead of the ordinary
SCL query engine. It can then query the enclosing block's constraint store:
module {
features = [chr]
}
import "StandardLibrary"
main =
select (?a,?b) where
Foo ?a
Bar ?b
where
True => Foo 1
True => Foo 2
True => Bar 3
True => Bar 4
A CHR select is a pure expression — it needs no Proc effect — and select first works
as usual, returning one solution rather than a list.
A select may also appear inside a rule head, which is the idiomatic way to express "…
and there is no fact such that …" in the absence of real negation:
when -Edge ?x ?y
[] = select ?z where
Edge ?z ?x
then print "removed \(?x) \(?y)"
chr feature actually changesfeatures = [chr] does not switch CHR on — rules work without it. What it does is swap
the meaning of four words in the lexer:
| Word | Without chr |
With chr |
|---|---|---|
ruleset |
an ordinary identifier | keyword — ruleset declaration |
select |
the ordinary SCL query | a CHR query |
rule |
keyword — mapping rule declaration | an ordinary identifier |
transformation |
keyword | an ordinary identifier |
Two consequences are easy to trip over. First, turning the feature on removes the
rule and transformation declarations from the module, so a module cannot mix CHR
rulesets with mapping rules. Second, select changes engine, and with it the order of
the results:
select (?a,?b) where
?a <- [1,2,3]
?b <- [2,3]
// without chr: [(1,2), (2,2), (3,2), (1,3), (2,3), (3,3)]
// with chr: [(1,2), (1,3), (2,2), (2,3), (3,2), (3,3)]
The results are the same set in a different order. If a module's existing select
expressions depend on that order, adding chr to its header will change their behaviour.
| Message | Cause |
|---|---|
| Constraint is applied with wrong number of parameters | arity does not match the constraint declaration |
| Couldn't resolve constraint X / Couldn't resolve relation X | a qualified name that is not a constraint or relation |
| Couldn't resolve ruleset X | include X v where X is not a ruleset |
| Existential variable ?x is referred only once… | warning; use _ if intended |
| New existential variables can be defined only in queries | a fresh ?x in a rule body |
| Relation must be declared if record syntax is used | record syntax on an inferred constraint |
| Relation V does not define field names | record syntax on a positional constraint |
| Field y not defined | a record constraint created without all its fields |
| Cannot solve the query | the head cannot be turned into a search plan, usually because nothing binds a variable |
| Only constraints can be marked for removal | - applied to something that is not a constraint |
| CHR negation is not yet supported | see below |
| Invalid CHR literal | a head literal the translator does not recognise |
Negation is not implemented. not Foo ?x in a head is rejected with CHR negation is
not yet supported. Use an empty nested select instead, as shown under CHR queries.
Constraints take no annotations. @private and friends in front of a constraint
are a syntax error.
CHR does not work outside module compilation. Rules written in an SCL script, in the
SCL Console, or in any other expression evaluated through ExpressionEvaluator parse and
type check, and then fail during code generation:
> True => Foo 1
Foo ?x => print ?x
InternalCompilerError: Didn't find type constructor Expression$1/CHR$1.
A CHR block compiles to a generated runtime class that belongs to the enclosing module, and an expression compiled on its own has no module to put it in. Put CHR rules in a module function and call that function from the script. This is tracked as issue #1426; see also 1.18 Modules vs. scripts.
Fact ordering is unspecified, as noted under Execution model.
The compiler's own regression fixtures are the most complete set of working CHR programs in the repository, covering sums, gcd, reachability, primes, topological sort, graph simplification, rulesets and CHR queries:
tests/org.simantics.scl.compiler.tests/src/org/simantics/scl/compiler/tests/scl/CHR*.scl
Each file contains the module text, a -- separator and the expected output.