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