The constraint store
The constraint store introduced in Constraints is a multiset of constraints, each one a suspended goal together with the hypotheses it carried and the variables that trigger its resumption. A constraint handling rule (Constraint handling rules for its syntax) inspects that multiset as a whole. What follows is what the store holds, and in what order its rules are tried.
Frozen variables
A constraint handling rule matches the store, it never unifies with it. The
whole point of declare_constraint is to stop guessing values for a hole,
not to let a CHR rule guess one instead. So every unification variable
occurring in a matched constraint is frozen: replaced with a fresh constant
that no rule can assign. A frozen variable F, with the bound variables
x and y in its scope, is shown to rules as uvar f [x, y] for a
fresh constant f. This reification is what lets a rule pattern name the
identity of a hole (K in uvar K _, see
Constraint handling rules) and read off its scope, with no
way of assigning it. Each matched constraint is frozen into its own,
disjoint space of names, so terms coming from two different constraints never
accidentally share a fresh constant.
The even/odd example from Constraints is fixed by exactly the CHR rule
already shown as syntax in Constraint handling rules:
even N and odd N, suspended on the same still-unknown N, freeze it
to the same constant, and the rule’s non-linear X matches only when both
constraints carry it, so the two are caught for what they are, one
contradiction, instead of passing as two unrelated, silent suspensions.
../code/incompatible-fixed.elpi:
1% The even/odd generators from incompatible.elpi (Constraints), plus the CHR
2% rule that catches the contradiction: it fires whenever the store holds
3% `even N` and `odd N` for the same, still-unknown `N`, matching them by the
4% identity of the frozen variable they share rather than unifying it.
5
6func even int.
7even (uvar as X) :- !, declare_constraint (even X) [X].
8even 0 :- !.
9even N :- N > 0, !, M is N - 2, even M.
10
11func odd int.
12odd (uvar as X) :- !, declare_constraint (odd X) [X].
13odd 1 :- !.
14odd N :- N > 1, !, M is N - 2, odd M.
15
16constraint even odd {
17 rule (even X) (odd X) <=> false.
18}
19
20main :-
21 ( even N, odd N, print "accepted (wrong!)"
22 ; print "rejected, no number is both" ).
rejected, no number is both
Triggers and clustering
A rule is only ever tried against constraints that could plausibly be
related: those whose trigger, the key list given to declare_constraint,
has a non-empty intersection with the constraint that was just declared or
resumed. A constraint keyed on _ is never resumed (_ can never be
assigned), but _ is a single shared placeholder, so passing it as one of
several keys (declare_constraint (even X) [_, X]) forces two otherwise
unrelated constraints to be considered together by CHR without expecting
either to trigger a resumption on its own. A constraint keyed on [] has
no trigger at all: it is never resumed, and, sharing nothing, no
multi-headed rule ever considers it jointly with another constraint. A
single-headed rule needs no partner, so it is still tried on such a
constraint, once, when the constraint is declared. The generalization step of
Hindley-Milner type inference relies on this: it keys its overbar
constraint on [] and generalizes it with a single-headed rule.
Application of CHR rules
As soon as a constraint C is declared or resumed, Elpi looks for a rule
to fire on it. It goes through the rules of C’s clique from top to
bottom. For each rule it gathers the store constraints whose trigger overlaps
C’s, and tries every way of matching the rule’s patterns against C
placed among enough of those others; C may land in any one of the
patterns, not only the first. The candidate constraints are frozen first
(see above). If they all match their patterns and the guard then succeeds,
the rule fires: the constraints matched to the right of the \ are removed
from the store, and the goal after <=>, if any, is run at once, ahead of
any other pending goal. The choice is committed at that point: no other
rule, and no other way of matching this one, is tried for C.
This is the refined operational semantics of Duck, Stuckey, García de la Banda & Holzbaur.
Symmetric CHR rules
The case most likely to surprise is a rule with no \. With two matching
constraints in the store, a two-pattern rule fires once per ordering of
them: removing nothing leaves both in the store, still eligible when the next
combination is tried.
../code/chr-permutations.elpi:
1% Two constraints share a trigger, so a 2-pattern rule is tried on every
2% permutation of them: "rule 1" fires once per ordering of (c 1, c 2).
3
4func c int.
5
6constraint c {
7 rule (c N) (c M) <=> (print "rule 1 on" N M).
8 rule (c N) <=> (print "rule 2 on" N).
9}
10
11main :-
12 print "declare c 1", declare_constraint (c 1) [_],
13 print "declare c 2", declare_constraint (c 2) [_].
declare c 1
rule 2 on 1
declare c 2
rule 1 on 2 1
rule 1 on 1 2
rule 2 on 2
rule 1 fires on (c 2) (c 1) and then again on (c 1) (c 2).
A rule whose two patterns are of the same predicate and which removes
nothing is symmetric, and it is this matching-without-removing shape that
gets retried on every ordering of a pair. Removing one of the two matched
constraints (rule (c N) \ (c M) <=> …) breaks the symmetry: the removed
copy is gone from the store before the second ordering is tried, so the rule
fires only once, on whichever ordering comes up first (here (c 2) (c 1),
keeping c 2 and removing c 1). That kept constraint stays in the
store:
../code/chr-permutations-fixed.elpi:
1% Same store as chr-permutations.elpi, but the first rule now removes one of
2% the two constraints it matches (`\`), so the second permutation has nothing
3% left to fire on: "rule 1" fires only once.
4
5func c int.
6
7constraint c {
8 rule (c N) \ (c M) <=> (print "rule 1 on" N M).
9 rule (c N) <=> (print "rule 2 on" N).
10}
11
12main :-
13 print "declare c 1", declare_constraint (c 1) [_],
14 print "declare c 2", declare_constraint (c 2) [_].
declare c 1
rule 2 on 1
declare c 2
rule 1 on 2 1
rule 2 on 2
A guard such as N < M breaks the symmetry equally well, by rejecting one
of the two orderings rather than removing a constraint. Since either fix
works, and which one is correct depends on the rule, Elpi does not warn about
a symmetric pattern on its own.
Cliques fix what a rule can see
A clique (Constraint handling rules) is the fixed set of
predicates a group of rules may talk about. Rule search only ever considers
the rules of C’s own clique, which is why two cliques must be disjoint: a
rule could otherwise be reached from two unrelated triggers. The context
filter widens only what a resumed goal remembers of the hypothetical rules
it was suspended under, not which CHR rules may act on it.
A global variable built on CHR
Elpi has no built-in global variable, but the constraint store, a place that
outlives individual goals and honours backtracking, makes it easy to build
one on top of CHR. The program below implements named global variables:
get and set take the variable’s name as a string, so several can
coexist. main initialises x and y, reads x, overwrites it,
and reads both again, so x’s two reads print the old value then the new
one while y is unchanged:
../code/global-state.elpi:
1% Named global variables, built on top of the constraint store: `value` holds
2% a variable's current content, `set` overwrites it, `get` reads it. The
3% string argument is the variable's name, so several can coexist.
4
5func set string, int.
6set Name I :- declare_constraint (set Name I) [_].
7
8func get string -> int.
9get Name I :- declare_constraint (get Name I) [_].
10
11func value string, int.
12value Name I :- declare_constraint (value Name I) [_].
13
14constraint set get value {
15 rule \ (set Name I) (value Name _) <=> (value Name I).
16 rule (value Name J) \ (get Name I) <=> (I = J).
17 rule (set Name _) <=> (halt "unknown global variable" Name).
18 rule (get Name _) <=> (halt "unknown global variable" Name).
19 rule (value Name _) (value Name _) <=> (halt "double initialization of" Name).
20}
21
22main :-
23 value "x" 2, % initialize x
24 value "y" 10, % and y
25 get "x" N, % read x
26 set "x" 3, % overwrite x
27 get "x" M, % read x again
28 get "y" Y, % y is untouched
29 print "x before:" N "x after:" M "y:" Y.
x before: 2 x after: 3 y: 10