2

I'm working on my own implementation of when/2.

Internally, when/2 needs to be able to—for a lack of a better word—"reify" ground/1. Doing this using attributed variables (in the style of SICStus-Prolog) is what this question is all about.

My implementation should work like the following "reference" implementation:

ground_t(Term,T) :-
   when(ground(Term), T=true).

In a fit of curiosity, I wrote the following little solver:

:- module(ground_t,[ground_t/2]).

:- use_module(library(atts)).
:- attribute ground_t/1.

ground_t(Term,T) :-
    (  ground(Term)
    -> T = true
    ;  term_variables(Term,Vs),
       create_mutable(Vs,MVs),
       attach_to_new_vars(Vs,MVs,T)
    ).

attach_to_new_vars([],_,_).
attach_to_new_vars([V|Vs],MVs,T) :-
    (  get_atts(V,+ground_t(Cs0))
    -> Cs = Cs0
    ;  Cs = []
    ),
    put_atts(V,+ground_t([MVs-T|Cs])),
    attach_to_new_vars(Vs,MVs,T).

% simple case: unify w/ground term
cliques_var_goals__ground([],_,[]).
cliques_var_goals__ground([MVs-T|Cs],Var,Goals) :-
    get_mutable(Vs0,MVs),
    list_eq_removed_atmost_once(Vs0,Var,Vs1),
    update_mutable(Vs1,MVs),
    (  Vs1 = []
    -> Goals = [T=true|Goals0]
    ;  Goals = Goals0
    ),
    cliques_var_goals__ground(Cs,Var,Goals0).

list_eq_removed_atmost_once([],_,[]).
list_eq_removed_atmost_once([X|Xs],E,Ys) :-
    (  X == E
    -> Ys = Xs         % remove item. do it at most once and stop
    ;  Ys = [X|Ys0],
       list_eq_removed_atmost_once(Xs,E,Ys0)
    ).

% general case: unify w/non-ground term
cliques_var_other__nonvar([],_,_).
cliques_var_other__nonvar([MVs-T|Cs],Var,Other) :-
    get_mutable(Vs0,MVs),
    term_variables(Vs0+Other,Vs1),
    term_variables(Other,New0),
    list_eq_removed_atmost_once(New0,Var,New),
    list_eq_removed_atmost_once(Vs1,Var,Vs2),
    attach_to_new_vars(New,MVs,T),
    update_mutable(Vs2,MVs),
    cliques_var_other__nonvar(Cs,Var,Other).

sort_all([]).
sort_all([MVs-_T|Xs]) :-
    get_mutable(Vs0,MVs),
    term_variables(Vs0,Vs),
    update_mutable(Vs,MVs),
    sort_all(Xs).

merge_cliques_post_unify(Cs0,Cs1,Other) :-
    append(Cs0,Cs1,Cs01),
    sort_all(Cs01),
    put_atts(Other,+ground_t(Cs01)).

verify_attributes(Var,Other,Goals) :-
    (  get_atts(Var,+ground_t(Cs0))
    -> (  ground(Other)
       -> cliques_var_goals__ground(Cs0,Var,Goals)
       ;  nonvar(Other)
       -> cliques_var_other__nonvar(Cs0,Var,Other),
          Goals = []
       ;  get_atts(Other,+ground_t(Cs1))
       -> Goals = [merge_cliques_post_unify(Cs0,Cs1,Other)]
       ;  put_atts(Other,+ground_t(Cs0)),
          Goals = []
       )
    ;  Goals = []
    ).

cliques_to_goal([]) --> [].
cliques_to_goal([MVs-T|Cs]) -->
    { get_mutable(Vs,MVs) },
    (  { Vs = []  } -> []
    ;  { Vs = [V] } -> [ground_t(V ,T)]
    ;                  [ground_t(Vs,T)]
    ),
    cliques_to_goal(Cs).

list_to_commalist([],true).
list_to_commalist([X],Y) :-
    !,
    X = Y.
list_to_commalist([X|Xs],(X,Y)) :-
    list_to_commalist(Xs,Y).

attribute_goals(Var) -->
    { get_atts(Var,+ground_t(Cs)) },
    cliques_to_goal(Cs).

attribute_goal(Var,Goal) :-
    phrase(attribute_goals(Var),Goals),
    list_to_commalist(Goals,Goal).

That was the easy part; now comes the hard part...

How do I go about testing if the behavior of the two solvers are equivalent—when performing multiple steps of variable aliasing and instantiation in a forest of shared terms?


Here are some simple manual tests that I did:

| ?- ground_t(X+Y,T1).
| ?- ground_t(X+Y,T1),X=f(U).
| ?- ground_t(X+Y,T1),X=f(U),U=Y.
| ?- ground_t(X+Y,T1),X=f(U),U=Y,Y=y.
| ?- ground_t(X+Y,T1),ground_t(X+Z,T2).
| ?- ground_t(X+Y,T1),ground_t(X+Z,T2),X = -U,Z = +V.
| ?- ground_t(X+Y,T1),ground_t(X+Z,T2),X = -U,Z = +U.
| ?- ground_t(X+Y,T1),ground_t(X+Z,T2),X = -U,Z = +U,U=u.
| ?- ground_t(X+Y,T1),ground_t(X+Z,T2),X = -U,Z = +U,U=u,Y=U.

Maybe you have a good idea for some nice corner cases... please share!

repeat
  • 18,496
  • 4
  • 54
  • 166
  • 1
    Which Prolog system are you using? For testing, maybe convert both forms to strings (write_canonical or similar) and compare them? Or, it's not difficult to write your own "equivalent" predicate by walking the terms (e.g., use =../2) – Peter Ludemann Nov 30 '22 at 23:45
  • 1
    @PeterLudemann: Looks like [SICStus](https://sicstus.sics.se/download_beta.html), at least the prompt is not the prompt of Scryer nor SWI. – false Dec 01 '22 at 09:24
  • @PeterLudemann. Yes, I'm targeting systems that offer a SICStus-style attributed-variable interface. The two main tricky parts when testing seem to be: (1) getting a "good" representation of the residual goals I can work with, (2) generating a series of successive instantiations that are interesting. – repeat Dec 01 '22 at 13:16

1 Answers1

2

The really hard part is first of all understanding what ground_t/2 is actually about. It is not reifying anything, for there is no false.

It seems to be some internal auxiliary predicate you need to implement when/2. If it is an internal predicate, it does not need to fulfill all the expectations we have about a real constraint. Most notably, your implementation (as well as your reference implementation using the built-in when/2) does not constrain all involved variables:

SICStus 4.8.0beta3 (x86_64-linux-glibc2.17): Mon Oct 17 20:11:03 UTC 2022
...
| ?- when(ground(A-B),T=true),copy_term(A,Ac,Ac_0),copy_term(B,Bc,Bc_0).
Ac_0 = prolog:trig_ground(Ac,[_A],[Ac,_A],_B,_B),
Bc_0 = true,
prolog:trig_ground(A,[B],[A,B],_C,_C),
prolog:when(_C,ground(A-B),user:(T=true)) ? ;
no
| ?- ground_t(A-B,T),copy_term(A,Ac,Ac_0),copy_term(B,Bc,Bc_0).
Ac_0 = ground_t:(ground_t([Ac,_A],_B),true),
Bc_0 = ground_t:true ? ;
no
| ?- ground_t(A-B,T).
ground_t([A,B],T) ? ;
no
| ?- 

So both do not really constrain B, although the ground_t:true kind of gives us a hint. Also, it seems that the goal ground_t([A,B],T) is only displayed when no copy_term/3 has been used.

Given such lax requirements, freeze/2 is all you need. No need to delve into atvs at all.

myground_t(Term, T) :-
    term_variables(Term, Vs),
    myvars_t(Vs, T).

myvars_t([], true).
myvars_t([V|Vs], T) :-
    freeze(V, myground_t([V|Vs], T)).

| ?- myground_t(A-B,T),copy_term(A,Ac,Ac_0),copy_term(B,Bc,Bc_0).
Ac_0 = prolog:freeze(Ac,user:myground_t([Ac,_A],_B)),
Bc_0 = true,
prolog:freeze(A,user:myground_t([A,B],T)) ? ;
no

(A good implementation of term_variables/2 could avoid recreating identical list suffixes. No, there is currently none I know of.) What is missing in this implementation?

Now to your actual question about testing. Testing coroutines and constraints requires quite a lot of compute. So one may ask in advance whether or not the implementation to be tested has been written in such a manner to avoid certain errors a priori. I did not read your code any further than term_variables_set/2 whose second argument depends on the very age of variables which means that it might work for one age and does not work for another one. It would not be the first. So testing gets here even more complex. Why this at all?

false
  • 10,264
  • 13
  • 101
  • 209
  • 1
    Here I got something curious: `| ?- ground_t(A-B,T), (copy_term(A,Ac,Acc);copy_term(B,Bc,Bcc)). Acc = ground_t:(ground_t([Ac,_A],_B),true) ? ; Bcc = ground_t:(ground_t([_A,Bc],_B),true) ? ; no` – repeat Dec 01 '22 at 12:49
  • 1
    At least your observation is consistent with: `| ?- ground_t(A-B,T), copy_term(B,Bc,Bc_0). Bc_0 = ground_t:(ground_t([_A,Bc],_B),true) ? ; no` – false Dec 01 '22 at 13:02
  • So using `term_variables/2` instead of `term_variables_set/2` would be a good move... Very good! (For some reason I believed that sorting the list of variables could help me detect some redundant internal sets.) – repeat Dec 01 '22 at 13:29
  • 1
    Ad `term_variables/2`: It's in Cor.2 not without reason. There were so many such problems. – false Dec 01 '22 at 13:35
  • 1
    The `ground_t:attribute_goal/2`implementation above is not idempotent, it is supposed to just bind the second argument to the attached goals, but the above implementation also modifies the attached data. This is why `copy_term/3` (which uses `M:attribute_goal/2`) misbehaves. – Per Mildner Dec 07 '22 at 14:27
  • @PerMildner: Please write it as an answer. – false Dec 07 '22 at 14:32