Basic CHR question

Hi there, sorry that my question may be too basic.
For the following CHR rule:

:- use_module(library(chr)).
:- chr_constraint try/1.

try(X) ==> writeln(guard), ground(X) | writeln(body).

If I assert a try(X), it gives this:

?- try(X).
guard
guard
try(X).

Why was the Guard checked twice for a single assertion?
Any comments appreciated! Thank you!

I asked claude, and the reason is a subtle bug. It is because we are showing the contents of the store at the end of the query. This inadvertenlty duplicates constraints in the store.

You can test this by adding the following to your code:

:- set_prolog_flag(chr_toplevel_show_store,false).

and it will work as expected.

I asked claude about a fix and it suggests replacing duplicate_term with copy_term in code called by $enumerate_constraints:

 Update(~/git/swipl-devel/packages/chr/chr_swi.pl)
  ⎿  Added 1 line, removed 1 line
      408      State = state(List2),
      409      (   call(Goal),
      410          arg(1, State, L),
      411 -        duplicate_term(Templ, New),                                                                                                            
      411 +        copy_term(Templ, New, _),                                                                                                              
      412          New = Templ,
      413          Cons = [New|_],
      414          nb_linkarg(2, L, Cons),

Claude verified the patch fixes the issue and reran the tests which passed successfully.

This also explains Tricky CHR behaviour - #3 by AZH

Hmm. Checking this fix, shows an issue. In that grounded constraints are showing incorrectly.

?- try(1).
guard
body
'chr normalize_attr'(_, _).

I’ll see if I can work out why!

That is surely wrong. The duplicate_term/2 is required to avoid backtracking destroying (part of) the term. Besides, making a copy without constraints is done using copy_term_nat/2, which is way cheaper.

So, to make a copy without constraints we’ll need both duplicate_term/2 and copy_term_nat/2. If we do that though, we do not copy the constraints and these may thus be subject to backtracking. I’m not a CHR expert (or even user). Anyone with deeper insights into this matter?

I’ve tested the following change, and it appears to work in the ground/unground case.

78 ?- try(1).
guard
body
try(1).

78 ?- try(X).
guard
try(X).

79 ?- try(1).
guard
body
try(1).

The proposed fix is:

  @@ -409,6 +409,8 @@ findallv(Templ, Goal, List, Tail) :-
       (   call(Goal),
           arg(1, State, L),
           duplicate_term(Templ, New),
  +        term_variables(New, NewVars),
  +        maplist(del_attrs, NewVars),
           New = Templ,
           Cons = [New|_],
           nb_linkarg(2, L, Cons),

This keeps the duplicate term, but prevents it being unified with store constraints when we hit the New = Templ unification, potentially (re)triggering rules.