-
-
Notifications
You must be signed in to change notification settings - Fork 167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inconsistent variable sharing behavior of attribute_goals
when called from frozen/2
vs. copy_term/3
#828
Comments
Thanks for the observations. attribute_goals//1 was introduced for copy_term/3. More recently frozen/2 was changed from just dealing with freeze/2 to be generic, compatible with SICStus. Here we cannot copy as we would get copies of the variables. I'm not that deep into these issues, but I think this implies that attribute_goals//1 should be documented to (possibly) receive the original variable and (thus) is not allowed to modify attributes. I guess that implies when/2 is broken when combined with frozen/2 (at least for disjunctions). I propose we wait with documenting these things until this is resolved. |
Apparently there are multiple parts of SWI's builtins/standard library that expect [debug] ?- Y #= X + 1, X = 3.
Y = 4,
X = 3.
[debug] ?- Y #= X + 1, frozen([X, Y], Frozen), X = 3.
ERROR: Uninstantiated argument expected, found processed (1-st argument)
ERROR: In:
ERROR: [19] put_attr(processed,clpfd_aux,queued)
ERROR: [18] clpfd:trigger_prop(propagator(pplus(3,1,_69498),processed)) at <builddir>/home/library/clp/clpfd.pl:3890
ERROR: [17] clpfd:trigger_props_([propagator(...,processed)]) at <builddir>/home/library/clp/clpfd.pl:3888
ERROR: [16] clpfd:trigger_props(fd_props([],[...],[])) at <builddir>/home/library/clp/clpfd.pl:3884
ERROR: [15] clpfd:attr_unify_hook(clpfd_attr(no,no,no,from_to(inf,sup),fd_props([],...,[])),3) at <builddir>/home/library/clp/clpfd.pl:7266
ERROR: [14] uhook(clpfd,clpfd_attr(no,no,no,from_to(inf,sup),fd_props([],...,[])),3) at <builddir>/home/boot/attvar.pl:86
ERROR: [13] call_all_attr_uhooks(att(clpfd,clpfd_attr(no,no,no,...,...),[]),3) at <builddir>/home/boot/attvar.pl:63
ERROR: [12] '$wakeup'(wakeup(att(clpfd,...,[]),3,[])) at <builddir>/home/boot/attvar.pl:58
ERROR: [11] 3=3 <foreign>
ERROR: [10] '<meta-call>'(user:user: ...) <foreign>
ERROR: [9] toplevel_call(user:user: ...) at <builddir>/home/boot/toplevel.pl:1115
Exception: (11) _67342{clpfd = ...}=3 ? And ?- dif(X, Y), frozen([X, Y], Frozen), frozen([X, Y], Frozen2), copy_term([X, Y], _, AttrGoals).
Frozen = dif(X, Y),
Frozen2 = true,
AttrGoals = []. (The correct result would be that Considering that (as you explained above) The alternative would be to switch to a different goal representation that can be rendered correctly without keeping state across |
😢 I'm afraid you are right. As by its original design If the above observation is correct the ideal way would be to follow this design. I'm afraid that will get nasty though and might require changing the internal constraints representation for some of the constraint libraries. I pushed 52274a5 which I think is a sensible (at least short term) solution that restores (I think) correctness. At least it passes the tests, the clpfd example and the diff example (both added to the tests). Please have a look. |
Right - on SICStus,
I agree that it would be better to always pass the original variable, once that's possible without breaking SWI's libraries. That way if an
For reference, here's how some of SICStus' goal representations look. Since SICStus | ?- X in 1..10, Y in 1..10, Y #= X + 1.
X in 1..9,
Y in 2..10 ? ;
no
| ?- when((ground(X) ; ground(Y)), Whened = true).
prolog:trig_ground(X,[],[X],_A,_B),
prolog:trig_ground(Y,[],[Y],_A,_C),
prolog:trig_or([_B,_C],_A,_A),
prolog:when(_A,(ground(X);ground(Y)),user:(Whened=true)) ? ;
no SWI could do something similar, but it would be a shame to lose the higher-quality output, and it might break SWI-specific code that depends on e. g. looking for CLPFD constraints in the goals. Would it make any sense to add a variant of
Yes, this seems to fix all the problems with |
Just for the record, copy_term/3 also passes the original variables. It uses findall/3 to get a copy of the derived term and goal and as findall/3 backtracks the changes by attribute_goal//1 are reverted. I think when with disjunctions still leaves residual variables, no? I think we can close this one. Please reopen should that be needed. |
Alright, I've figured out where the unexpected residual variables are coming from, and it is indeed caused by the changes in 52274a5. It happens when backtracking past a ?- call_residue_vars((X in 1..2, frozen(X, FX) ; true), Vars).
FX = clpfd:(X in 1..2),
Vars = [X],
X in 1..2 ;
Vars = [].
?- call_residue_vars((X in 1..2, (frozen(X, FX) ; true)), Vars).
FX = clpfd:(X in 1..2),
Vars = [X],
X in 1..2 ;
Vars = [X],
X in 1..2. And here's the incorrect behavior after the change, where backtracking past ?- call_residue_vars((X in 1..2, frozen(X, FX) ; true), Vars).
FX = clpfd:(X in 1..2),
Vars = [X],
X in 1..2 ;
Vars = [_9660],
_9660 in 1..2.
?- call_residue_vars((X in 1..2, (frozen(X, FX) ; true)), Vars).
FX = clpfd:(X in 1..2),
Vars = [X],
X in 1..2 ;
Vars = [_19150, X],
X in 1..2,
_19150 in 1..2. I'm guessing this comes from the |
I see. Pushed c536f79 to handle this, using findall/3 rather than nb_setarg/3. The duplicated term indeed contains residual variables. Under normal execution this is not a problems as GC will take care of the term rather than backtracking. call_residue_vars/2 blocks GC for resitual vars though, so it is left. findall/3 also copies the term, but off the stacks and back onto the stacks rather than using the frozen bar mechanism that supports non-backtrackable data on the stacks. Now backtracking can simply discard the copy. Tricky stuff 😢 Please give it a try. Should be functionally equivalent. At least the tests still pass. If you do more testing, please create a PR with them (they are in |
This seems to have fixed the problem with the residual variables, thank you! Our unit tests are much happier now too 🙂 Yes, I've been meaning to write down my one-off test calls as proper plunit tests, but haven't gotten around to it yet... Once I do, I'll send a PR with the relevant ones. |
I ran into this while looking into #818.
attribute_goals//1
is called from bothfrozen/2
andcopy_term/3
, but with one small difference:frozen/2
passes the attvar in question directly toattribute_goals//1
, whereascopy_term/3
passes in a copy of the attvar (as if created bycopy_term/2
).This difference is relevant if
attribute_goals//1
modifies the attvar's attributes in any way, either by putting/removing attributes or by binding variables inside existing attributes. If such anattribute_goals//1
implementation is called fromcopy_term/3
, these changes have no lasting effect, because the received attvar and its attributes are freshly copied, and all attributes are deleted from the copied variable beforecopy_term/3
returns. On the other hand, ifattribute_goals//1
is called fromfrozen/2
, it receives the real variable, so any changes to the attributes remain in place and are not undone whenfrozen/2
returns.Here's a contrived example. Every time this
attribute_goals//1
is called, it adds an extras(X)
compound around the attribute value.If you use this with
copy_term/3
, the modifications to the attribute are not visible externally (every goal shows a copy of the originalattr(Attr)
value):But with
frozen/2
, the modifications are permanent (every goal shows one mores
in the attribute than the previous goal):The documentation for
attribute_goals//1
isn't very clear about this, but it says "Its argument is that variable.", which implies that the received variable should always be the original variable and not a temporary copy. I don't have any strong opinions about which behavior is better, but IMO either there should be a consistent documented behavior, or the documentation should explicitly say that the exact behavior is undefined and thatattribute_goals//1
shouldn't modify attributes.FWIW, on SICStus (tested with version 4.5.1) the equivalent hook predicate
attribute_goal/2
always receives the original variable and never a copy. But the SICStus documentation also doesn't explicitly say anything about this behavior or about modifying attributes insideattribute_goal/2
.The text was updated successfully, but these errors were encountered: