[{"data":1,"prerenderedAt":682},["ShallowReactive",2],{"blog-constraint-handling-rules":3},{"id":4,"title":5,"body":6,"date":673,"description":674,"draft":675,"extension":676,"meta":677,"navigation":486,"path":678,"seo":679,"stem":680,"__hash__":681},"blog/blog/constraint-handling-rules.md","Constraint Handling Rules: Propagation Instead of Search",{"type":7,"value":8,"toc":664},"minimark",[9,13,28,31,36,39,52,55,70,73,80,115,134,148,164,170,181,184,189,204,209,229,234,246,251,254,257,269,272,276,296,330,333,341,344,348,351,357,401,407,410,416,446,450,453,461,464,467,499,524,533,536,540,543,546,549,552,569,572,576,579,582,585,588,591,594,598,660],[10,11,12],"p",{},"I have been looking at relational programming, where a query searches for substitutions that satisfy a relation. Constraint Handling Rules takes a different approach. It keeps a multiset of constraints and repeatedly applies guarded rewrite rules to that store. By default it does not search over alternatives; it commits to a rule and continues. That changes both the programming model and the failure modes.",[10,14,15,16,27],{},"The name sounds narrower than the language is. CHR began as a way to add user-defined constraint solvers to a host language. It became a general-purpose rule language, used for type systems, testing and verification, scheduling, and classic algorithms ",[17,18,21],"sup",{"className":19},[20],"citation",[22,23,26],"a",{"href":24,"ariaLabel":25},"#ref-2","Reference 2","[2]",". It is usually embedded in Prolog, Haskell, Java, or C, but it is not a small add-on. It is a full computational model.",[10,29,30],{},"This post is a first pass at that model, and at what it means for relational computation.",[32,33,35],"h2",{"id":34},"the-language-by-example","The language, by example",[10,37,38],{},"Suppose you know three things:",[40,41,42,46,49],"ul",{},[43,44,45],"li",{},"A is less than or equal to B.",[43,47,48],{},"B is less than or equal to C.",[43,50,51],{},"C is less than or equal to A.",[10,53,54],{},"What follows? All three must be equal. A cycle of ≤ forces equality at every edge. That is the kind of conclusion CHR is designed to derive.",[10,56,57,58,62,63,62,66,69],{},"In CHR, each fact is a constraint: ",[59,60,61],"code",{},"leq(A,B)",", ",[59,64,65],{},"leq(B,C)",[59,67,68],{},"leq(C,A)",". The engine keeps the current constraints in a multiset called the store. The store starts with those three constraints.",[10,71,72],{},"A CHR rule is a pattern that says how to change the store. It has a head, an optional guard, and a body. The head matches one or more constraints in the store. The guard is a condition written in the host language. If the head matches and the guard succeeds, the rule fires and its body updates the store.",[10,74,75,76,79],{},"The ",[59,77,78],{},"leq"," program uses four rules:",[81,82,87],"pre",{"className":83,"code":84,"language":85,"meta":86,"style":86},"language-prolog shiki shiki-themes github-light github-dark","reflexivity  @ leq(X,X) \u003C=> true.\nantisymmetry @ leq(X,Y), leq(Y,X) \u003C=> X = Y.\nidempotence  @ leq(X,Y) \\ leq(X,Y) \u003C=> true.\ntransitivity @ leq(X,Y), leq(Y,Z) ==> leq(X,Z).\n","prolog","",[59,88,89,97,103,109],{"__ignoreMap":86},[90,91,94],"span",{"class":92,"line":93},"line",1,[90,95,96],{},"reflexivity  @ leq(X,X) \u003C=> true.\n",[90,98,100],{"class":92,"line":99},2,[90,101,102],{},"antisymmetry @ leq(X,Y), leq(Y,X) \u003C=> X = Y.\n",[90,104,106],{"class":92,"line":105},3,[90,107,108],{},"idempotence  @ leq(X,Y) \\ leq(X,Y) \u003C=> true.\n",[90,110,112],{"class":92,"line":111},4,[90,113,114],{},"transitivity @ leq(X,Y), leq(Y,Z) ==> leq(X,Z).\n",[10,116,117,118,121,122,125,126,129,130,133],{},"Read ",[59,119,120],{},"transitivity"," first. Its head matches two constraints: ",[59,123,124],{},"leq(X,Y)"," and ",[59,127,128],{},"leq(Y,Z)",". If both are in the store, the rule adds ",[59,131,132],{},"leq(X,Z)",". The original two constraints stay. That is a propagation rule: it adds a consequence.",[10,135,136,139,140,143,144,147],{},[59,137,138],{},"reflexivity"," is a simplification rule. If the store contains ",[59,141,142],{},"leq(X,X)",", the rule replaces it with ",[59,145,146],{},"true",", which removes it. The constraint says nothing useful.",[10,149,150,153,154,125,156,159,160,163],{},[59,151,152],{},"antisymmetry"," is also a simplification rule, but it matches two constraints. If the store contains both ",[59,155,124],{},[59,157,158],{},"leq(Y,X)",", the rule removes both and adds ",[59,161,162],{},"X = Y",". The host language handles the equality.",[10,165,166,169],{},[59,167,168],{},"idempotence"," is a simpagation rule. It matches two copies of the same constraint. The copy before the backslash is kept; the copy after it is removed. This is why the store is a multiset, not a set: duplicate constraints can exist, and the rule can remove one while keeping the other.",[171,172,175,176],"figure",{"className":173},[174],"diagram-image","\n  ",[177,178],"img",{"src":179,"alt":180},"/blog/diagrams/chr-rule-types.png","Simplification, propagation, and simpagation rules",[10,182,183],{},"Now run the example. Start with:",[10,185,186],{},[59,187,188],{},"leq(A,B), leq(B,C), leq(C,A)",[10,190,191,192,194,195,125,197,199,200,203],{},"One derivation goes like this. ",[59,193,120],{}," fires on ",[59,196,65],{},[59,198,68],{},", adding ",[59,201,202],{},"leq(B,A)",". The store is now:",[10,205,206],{},[59,207,208],{},"leq(A,B), leq(B,C), leq(C,A), leq(B,A)",[10,210,211,213,214,125,216,218,219,222,223,225,226,203],{},[59,212,152],{}," matches ",[59,215,61],{},[59,217,202],{},". It removes both and adds ",[59,220,221],{},"A = B",". The equality rewrites the remaining ",[59,224,65],{}," to ",[59,227,228],{},"leq(A,C)",[10,230,231],{},[59,232,233],{},"leq(A,C), leq(C,A), A = B",[10,235,236,213,238,125,240,218,242,245],{},[59,237,152],{},[59,239,228],{},[59,241,68],{},[59,243,244],{},"A = C",". The final store is:",[10,247,248],{},[59,249,250],{},"A = B, A = C",[10,252,253],{},"The cycle of inequalities has been reduced to equalities. No rule applies to the equalities, so the derivation stops.",[10,255,256],{},"That is the basic shape of CHR. The program is a set of local rules. The engine keeps applying rules until no rule applies—or continues indefinitely if the program does not terminate. The rules are declarative, but the derivation is operational.",[10,258,259,260,268],{},"Once a rule fires, the engine commits. It does not backtrack over the choice of rule. If the guard fails, it tries the next rule. If no rule applies, the constraint is suspended ",[17,261,263],{"className":262},[20],[22,264,267],{"href":265,"ariaLabel":266},"#ref-3","Reference 3","[3]",".",[10,270,271],{},"That commitment is the first big difference from a search-based relational engine. A search-based engine explores alternatives and may expose multiple answers. CHR instead commits to a rewrite and continues.",[32,273,275],{"id":274},"what-the-rules-mean","What the rules mean",[10,277,278,279,287,292,293,295],{},"The rules have a logical reading ",[17,280,282],{"className":281},[20],[22,283,286],{"href":284,"ariaLabel":285},"#ref-1","Reference 1","[1]",[17,288,290],{"className":289},[20],[22,291,26],{"href":24,"ariaLabel":25},". For the ",[59,294,78],{}," program, it is easy to state:",[40,297,298,308,319],{},[43,299,300,302,303,305,306,268],{},[59,301,138],{}," says ",[59,304,142],{}," is equivalent to ",[59,307,146],{},[43,309,310,302,312,125,314,316,317,268],{},[59,311,152],{},[59,313,124],{},[59,315,158],{}," together are equivalent to ",[59,318,162],{},[43,320,321,302,323,125,325,327,328,268],{},[59,322,120],{},[59,324,124],{},[59,326,128],{}," imply ",[59,329,132],{},[10,331,332],{},"That is the declarative meaning. It is what the rules say, independent of the order in which the engine applies them.",[10,334,335,337,338,340],{},[59,336,168],{}," shows where the logical reading and the operational reading start to separate. Logically, a duplicate ",[59,339,124],{}," adds nothing; the truth is already there. Operationally, the store is a multiset, so the duplicate exists as a separate constraint. The rule removes one copy and keeps the other. The logic sees redundancy; the engine sees a multiset.",[10,342,343],{},"That gap matters. The logical reading tells you what the rules mean. The operational semantics tells you how the store changes. They are related, but they are not the same. A rule can be logically true and still produce a bad derivation if it loops or if the rule set is not confluent. Confluence means the final store does not depend on the order in which rules are applied; without it, two rule orders can end in different stores.",[32,345,347],{"id":346},"how-it-runs","How it runs",[10,349,350],{},"The previous section described the rules. This section describes what the engine does with them.",[10,352,353,354,356],{},"Start again with the store ",[59,355,188],{},". The engine does not apply all rules at once. It picks a constraint, makes it active, and tries the rules in order. The other constraints in the store are passive: they can be partners if a rule needs more than one constraint in its head.",[10,358,359,360,362,363,365,366,368,369,372,373,375,376,378,379,381,382,385,386,389,390,392,393,395,396,125,398,400],{},"Suppose ",[59,361,65],{}," is active. ",[59,364,138],{}," fails because the two arguments differ. ",[59,367,152],{}," looks for ",[59,370,371],{},"leq(C,B)","; it is not there. ",[59,374,168],{}," looks for a duplicate ",[59,377,65],{},"; there is none. ",[59,380,120],{}," looks for a constraint of the form ",[59,383,384],{},"leq(C,Z)"," or ",[59,387,388],{},"leq(X,B)",". It finds ",[59,391,68],{},", so the rule fires and adds ",[59,394,202],{},". The original ",[59,397,65],{},[59,399,68],{}," stay in the store.",[10,402,403,404,406],{},"That is one step. The new ",[59,405,202],{}," becomes active, and the engine repeats the process. It tries the rules in order, finds a partner if needed, checks the guard, and either fires a rule or suspends the constraint. When a rule fires, its body is executed immediately. The engine commits to the rule application: it does not undo that choice through backtracking. If the active constraint remains in the store, it may later resume and try subsequent rule occurrences.",[10,408,409],{},"If no rule applies, the constraint is suspended. It stays in the store and can be used as a passive partner for another active constraint. If a variable inside it becomes bound later, the constraint is reactivated and the rules are tried again. That is how a constraint can become relevant after it was suspended.",[10,411,412,413,415],{},"The engine's state is more than the constraint store. It also has a goal, which is the constraints that have not been introduced yet; a built-in store, which holds host constraints such as ",[59,414,221],{},"; and a propagation history. The propagation history records which rule has already fired on which constraints. Without it, a propagation rule like transitivity could fire on the same constraints repeatedly and add the same consequence forever. The history prevents that specific loop. It does not guarantee termination in general.",[10,417,418,419,424,429,430,435,440,441,268],{},"The theoretical semantics lets any applicable rule fire. The refined semantics used by most implementations fixes an order: a new constraint becomes active, rules are tried in program order, and matching partners are taken from the suspended store ",[17,420,422],{"className":421},[20],[22,423,26],{"href":24,"ariaLabel":25},[17,425,427],{"className":426},[20],[22,428,267],{"href":265,"ariaLabel":266},". That makes behavior predictable, but it also means rule order is part of the program's operational behavior. The declarative reading is order-independent: it is a set of logical formulas with no notion of which rule fires first ",[17,431,433],{"className":432},[20],[22,434,286],{"href":284,"ariaLabel":285},[17,436,438],{"className":437},[20],[22,439,26],{"href":24,"ariaLabel":25},". The implementation is not, and CHR programs can rely on that order for correctness or complexity ",[17,442,444],{"className":443},[20],[22,445,26],{"href":24,"ariaLabel":25},[32,447,449],{"id":448},"the-costs","The costs",[10,451,452],{},"The first cost is confluence. If the final store can depend on rule order, the program is not confluent, and different valid derivations may produce different results. That is the central correctness question, and it is not automatic.",[10,454,455,456,268],{},"The second is termination. Simplification rules remove constraints, so a ranking function can sometimes prove termination. Propagation rules add constraints without removing them, which makes termination harder. The propagation history prevents trivial loops, but it does not guarantee termination ",[17,457,459],{"className":458},[20],[22,460,26],{"href":24,"ariaLabel":25},[10,462,463],{},"The third is complexity. Multi-headed rules have to match several constraints at once. Guards have to be checked. The store has to be updated. The number of rule applications is the first-order cost, but the matching cost can dominate. CHR has a body of work on complexity analysis, but there is no free lunch.",[10,465,466],{},"The fourth is composition. Composition just means putting two independently written rule sets in the same store and letting them interact. The rules are local, but confluence and termination are properties of the whole program. Two rule sets can each be confluent and terminating on their own and still misbehave when combined. A minimal example:",[81,468,470],{"className":83,"code":469,"language":85,"meta":86,"style":86},"% rule set A\np(X) ==> q(X).\n\n% rule set B\nq(X) ==> p(X).\n",[59,471,472,477,482,488,493],{"__ignoreMap":86},[90,473,474],{"class":92,"line":93},[90,475,476],{},"% rule set A\n",[90,478,479],{"class":92,"line":99},[90,480,481],{},"p(X) ==> q(X).\n",[90,483,484],{"class":92,"line":105},[90,485,487],{"emptyLinePlaceholder":486},true,"\n",[90,489,490],{"class":92,"line":111},[90,491,492],{},"% rule set B\n",[90,494,496],{"class":92,"line":495},5,[90,497,498],{},"q(X) ==> p(X).\n",[10,500,501,502,505,506,508,509,505,511,513,514,516,517,62,519,516,521,523],{},"On its own, rule set A terminates: it adds ",[59,503,504],{},"q"," from ",[59,507,10],{}," and stops. On its own, rule set B terminates: it adds ",[59,510,10],{},[59,512,504],{}," and stops. Put them in the same store and they loop. ",[59,515,10],{}," adds ",[59,518,504],{},[59,520,504],{},[59,522,10],{},", and each new constraint has a new identity, so the propagation history does not recognize it as the same constraint and the cycle continues.",[10,525,526,527,532],{},"Composition can also break confluence. If two rule sets simplify the same constraint differently, the final store depends on which rule fires first. Sneyers et al. discuss this as a separate problem: rule sets that are confluent and terminating alone can lose those properties when combined, and preserving termination is especially hard ",[17,528,530],{"className":529},[20],[22,531,26],{"href":24,"ariaLabel":25},". That is the composition cost.",[10,534,535],{},"The hard part is not writing a rule. It is predicting what happens when rules interact.",[32,537,539],{"id":538},"what-chr-gets-right","What CHR gets right",[10,541,542],{},"CHR treats constraints as partial information. A constraint in the store is not a function call waiting for arguments. It is a condition that may be simplified, combined, or propagated. That is close to the relational picture: the store is a shared body of partial knowledge, and the rules are local ways of drawing consequences.",[10,544,545],{},"The rules are compositional in form. Each rule is a small pattern; if execution terminates, the engine reaches a fixed point where no rule applies. That is a clean picture of computation as local rewriting.",[10,547,548],{},"CHR also shows that a rule-based constraint language can be practical without a search tree. The cost moves to confluence, termination, matching, and rule order. That is a different engineering problem from search in relational computation more generally, and it is worth studying on its own terms.",[10,550,551],{},"Finally, CHR shows the value of a host language. It borrows data types, arithmetic, and equality from the host. That is why the core can stay small. CHR pushes the idea further: the host provides the built-in constraint solver, and CHR provides the user-defined rules.",[10,553,554,555,560,568],{},"CHR is also unexpectedly general. It is Turing-complete, but that is the weaker claim. Turing completeness says a language can compute anything computable; it says nothing about cost. Sneyers et al. report a stronger result: complexity-wise completeness. For every RAM algorithm that uses at least as much time as space, there is a CHR program that, in the K.U.Leuven CHR system, runs within a constant factor of the original time and space complexity ",[17,556,558],{"className":557},[20],[22,559,26],{"href":24,"ariaLabel":25},[17,561,563],{"className":562},[20],[22,564,567],{"href":565,"ariaLabel":566},"#ref-4","Reference 4","[4]",". Roughly: for ordinary algorithms whose running time is at least as large as their memory use, CHR can match their asymptotic cost.",[10,570,571],{},"That is a stronger claim than it sounds. It means CHR is not just expressive; it can simulate ordinary algorithms without an asymptotic penalty. The multiset-rewriting style does not inherently cost an extra factor of time or space. The constants may be large, and the CHR program may be elaborate, but the model itself is not asymptotically handicapped. That is a remarkable result for a language built on local rewrite rules. It also changes the question: the hard part is not whether a rule-based constraint language can be efficient in principle, but whether a particular program and implementation can realize that efficiency in practice.",[32,573,575],{"id":574},"what-it-leaves-open","What it leaves open",[10,577,578],{},"The first open question is search. CHR is committed-choice. It does not explore alternatives by default. If a problem needs search, the host language or an extension has to provide it. Can propagation and search coexist without giving up the declarative reading?",[10,580,581],{},"The second is composition. Local rules do not guarantee global properties. Can confluence and termination be checked incrementally as rule sets are composed? Or is composition always a global analysis problem?",[10,583,584],{},"The third is the relationship between the logical and operational readings. CHR has both, and they are not the same. Which one is the specification? The answer depends on what you are building: a solver, a type checker, or a general-purpose system.",[10,586,587],{},"These are the questions I want to pursue. CHR is a mature model of rule-based constraint computation: rules propagate and simplify a shared store instead of searching a space of substitutions. The price is confluence, termination, and committed choice.",[10,589,590],{},"This is exploratory, not a claim that CHR is the right model. The value is in the contrast. CHR shows a different way to organize computation and cost, and that contrast is useful even if the final design looks like neither CHR nor a search-based relational language.",[10,592,593],{},"The interesting thing is not that one model is better. It is that they put control in different places: a search-based relational engine explores alternatives; CHR commits to a rule and rewrites the store. A compositional system has to decide which kind of control it wants.",[32,595,597],{"id":596},"references","References",[599,600,601,619,634,645],"ol",{},[43,602,603,606,607,613,614,618],{},[90,604],{"id":605},"ref-1","Frühwirth, T. (1998). ",[22,608,612],{"href":609,"rel":610},"https://doi.org/10.1016/S0743-1066(98)10005-5",[611],"nofollow","Theory and Practice of Constraint Handling Rules",". ",[615,616,617],"em",{},"Journal of Logic Programming",", 37(1–3), 95–138.",[43,620,621,624,625,613,630,633],{},[90,622],{"id":623},"ref-2","Sneyers, J., Van Weert, P., Schrijvers, T., and De Koninck, L. (2010). ",[22,626,629],{"href":627,"rel":628},"https://arxiv.org/abs/0906.4474",[611],"As Time Goes By: Constraint Handling Rules",[615,631,632],{},"Theory and Practice of Logic Programming",", 10(1), 1–47.",[43,635,636,639,640,268],{},[90,637],{"id":638},"ref-3","SWI-Prolog. ",[22,641,644],{"href":642,"rel":643},"https://www.swi-prolog.org/pldoc/man?section=chr-syntaxandsemantics",[611],"CHR Syntax and Semantics",[43,646,647,650,651,613,656,659],{},[90,648],{"id":649},"ref-4","Sneyers, J., Schrijvers, T., and Demoen, B. (2009). ",[22,652,655],{"href":653,"rel":654},"https://doi.org/10.1145/1462166.1462169",[611],"The Computational Power and Complexity of Constraint Handling Rules",[615,657,658],{},"ACM Transactions on Programming Languages and Systems",", 31(2).",[661,662,663],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":86,"searchDepth":99,"depth":99,"links":665},[666,667,668,669,670,671,672],{"id":34,"depth":99,"text":35},{"id":274,"depth":99,"text":275},{"id":346,"depth":99,"text":347},{"id":448,"depth":99,"text":449},{"id":538,"depth":99,"text":539},{"id":574,"depth":99,"text":575},{"id":596,"depth":99,"text":597},"2026-11-07T18:00:00","CHR is a committed-choice constraint programming language built on guarded multiset rewrite rules. It makes a different tradeoff from search-based relational programming: constraints propagate, rules commit, and the hard part is confluence, not search.",false,"md",{},"/blog/constraint-handling-rules",{"title":5,"description":674},"blog/constraint-handling-rules","8XTF9eH86qwUM1hlilSYZu_FLhjUB5Hnyg0ecq7Ehbs",1794236067063]