|
| 1 | +/* Part of SWI-Prolog |
| 2 | +
|
| 3 | + Author: Jan Wielemaker |
| 4 | + E-mail: jan@swi-prolog.org |
| 5 | + WWW: http://www.swi-prolog.org |
| 6 | + Copyright (c) 2026, SWI-Prolog Solutions b.v. |
| 7 | + All rights reserved. |
| 8 | +
|
| 9 | + Redistribution and use in source and binary forms, with or without |
| 10 | + modification, are permitted provided that the following conditions |
| 11 | + are met: |
| 12 | +
|
| 13 | + 1. Redistributions of source code must retain the above copyright |
| 14 | + notice, this list of conditions and the following disclaimer. |
| 15 | +
|
| 16 | + 2. Redistributions in binary form must reproduce the above copyright |
| 17 | + notice, this list of conditions and the following disclaimer in |
| 18 | + the documentation and/or other materials provided with the |
| 19 | + distribution. |
| 20 | +
|
| 21 | + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 22 | + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 23 | + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| 24 | + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
| 25 | + COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
| 26 | + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
| 27 | + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| 28 | + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| 29 | + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| 30 | + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
| 31 | + ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| 32 | + POSSIBILITY OF SUCH DAMAGE. |
| 33 | +*/ |
| 34 | + |
| 35 | +:- module(test_dif_random, |
| 36 | + [ test_dif_random/0, |
| 37 | + test_dif_random/1 % +Iterations |
| 38 | + ]). |
| 39 | +:- use_module(library(dif)). |
| 40 | +:- use_module(library(random_terms)). |
| 41 | +:- use_module(library(random)). |
| 42 | +:- use_module(library(lists)). |
| 43 | +:- use_module(library(plunit)). |
| 44 | + |
| 45 | +/** <module> Randomised property test for dif/2 |
| 46 | +
|
| 47 | +Take a random ground term T. Produce a generalisation G by replacing |
| 48 | +some subterms of T with fresh variables (each replacement recorded as |
| 49 | +`Var=Value`, so that unifying all replacements makes G structurally |
| 50 | +equal to T). Some of those variables are then aliased so that G also |
| 51 | +carries sharing constraints. Post `dif(G, T)` and drive it with a |
| 52 | +randomly chosen mix of |
| 53 | +
|
| 54 | + - "match" bindings that push G towards T, |
| 55 | + - one "clash" binding that forces a mismatch, or |
| 56 | + - leaving some variables unbound. |
| 57 | +
|
| 58 | +The predicted outcome is decidable from the plan itself: |
| 59 | +
|
| 60 | + - dif(G, T) must fail iff the plan drives G to be structurally == T |
| 61 | + (all replacements applied with their matching values, and no |
| 62 | + alias-conflict). |
| 63 | + - dif(G, T) must succeed in every other case: either a clash was |
| 64 | + applied, or some replacement was left unbound (so dif stays |
| 65 | + delayed), or the alias forced two positions to hold values that |
| 66 | + are decidably unequal in T. |
| 67 | +
|
| 68 | +Any deviation is reported with the offending T, G, plan and observed |
| 69 | +result — a minimal regression case ready to add to test_dif.pl. |
| 70 | +*/ |
| 71 | + |
| 72 | +test_dif_random :- |
| 73 | + run_tests(dif_random). |
| 74 | + |
| 75 | +test_dif_random(N) :- |
| 76 | + forall(between(1, N, _), check_scenario). |
| 77 | + |
| 78 | +:- begin_tests(dif_random). |
| 79 | + |
| 80 | +test(scenario, [forall(between(1, 500, _))]) :- |
| 81 | + check_scenario. |
| 82 | + |
| 83 | +:- end_tests(dif_random). |
| 84 | + |
| 85 | +check_scenario :- |
| 86 | + random_ground_term(T), |
| 87 | + generalize(T, G, Bindings0), |
| 88 | + maybe_alias_bindings(Bindings0, Bindings, AliasBlocksMatch), |
| 89 | + plan(Bindings, Plan, Expected0), |
| 90 | + ( AliasBlocksMatch == true |
| 91 | + -> Expected = ok % G can never equal T ⇒ dif always holds |
| 92 | + ; Expected = Expected0 |
| 93 | + ), |
| 94 | + run(G, T, Plan, Observed), |
| 95 | + ( Observed == Expected |
| 96 | + -> true |
| 97 | + ; report(scenario, T, G, Plan, Expected, Observed), |
| 98 | + fail |
| 99 | + ). |
| 100 | + |
| 101 | +random_ground_term(T) :- |
| 102 | + random_between(2, 5, Depth), |
| 103 | + random_between(1, 3, Arity), |
| 104 | + random_term(T, |
| 105 | + [ depth(Depth), max_arity(Arity), |
| 106 | + w_var(0), w_cycle(0) |
| 107 | + ]). |
| 108 | + |
| 109 | +%! generalize(+T, -G, -Bindings) is det. |
| 110 | +% |
| 111 | +% G is a fresh term structurally equal to T except that a random |
| 112 | +% subset of subterms has been replaced by fresh variables. Bindings |
| 113 | +% is the list `[Var=Value, ...]` such that applying every unification |
| 114 | +% makes G unifiable-with-and-then-equal-to T. |
| 115 | + |
| 116 | +generalize(T, G, Bs) :- |
| 117 | + generalize_(T, G, Bs, []). |
| 118 | + |
| 119 | +generalize_(T, G, Bs0, Bs) :- |
| 120 | + ( maybe(0.3) |
| 121 | + -> G = _V, |
| 122 | + Bs0 = [G=T | Bs] |
| 123 | + ; compound(T) |
| 124 | + -> compound_name_arguments(T, F, As), |
| 125 | + generalize_args(As, Gs, Bs0, Bs), |
| 126 | + compound_name_arguments(G, F, Gs) |
| 127 | + ; G = T, |
| 128 | + Bs0 = Bs |
| 129 | + ). |
| 130 | + |
| 131 | +generalize_args([], [], Bs, Bs). |
| 132 | +generalize_args([A|As], [G|Gs], Bs0, Bs) :- |
| 133 | + generalize_(A, G, Bs0, Bs1), |
| 134 | + generalize_args(As, Gs, Bs1, Bs). |
| 135 | + |
| 136 | +%! maybe_alias_bindings(+Bindings0, -Bindings, -AliasBlocksMatch) is det. |
| 137 | +% |
| 138 | +% Optionally alias two variables in Bindings0 so that G carries a |
| 139 | +% sharing constraint (the same var appears at two positions). |
| 140 | +% AliasBlocksMatch is `true` when two aliased positions carry |
| 141 | +% decidably different values in T — G can then never equal T, so |
| 142 | +% dif(G, T) is satisfied regardless of the plan. |
| 143 | + |
| 144 | +maybe_alias_bindings(Bindings0, Bindings, AliasBlocksMatch) :- |
| 145 | + ( Bindings0 = [V1=Val1 | Rest0], |
| 146 | + select(V2=Val2, Rest0, Rest), |
| 147 | + maybe(0.4) |
| 148 | + -> V1 = V2, |
| 149 | + Bindings = [V1=Val1 | Rest], |
| 150 | + ( Val1 \== Val2, ground(Val1), ground(Val2) |
| 151 | + -> AliasBlocksMatch = true |
| 152 | + ; AliasBlocksMatch = false |
| 153 | + ) |
| 154 | + ; Bindings = Bindings0, |
| 155 | + AliasBlocksMatch = false |
| 156 | + ). |
| 157 | + |
| 158 | +%! plan(+Bindings, -Plan, -Expected) is det. |
| 159 | +% |
| 160 | +% Pick a random subset of `Bindings` (in some order) as the plan. |
| 161 | +% Optionally swap exactly one of the picked bindings for a clash |
| 162 | +% value. Expected is decidable from the plan: |
| 163 | +% |
| 164 | +% * `fail` iff the plan is a full match sequence covering every |
| 165 | +% unique variable — that binds G structurally to T. |
| 166 | +% * `ok` in every other case. |
| 167 | + |
| 168 | +plan([], [], fail) :- !. % G identical to T |
| 169 | +plan(Bindings, Plan, Expected) :- |
| 170 | + unique_vars(Bindings, UVs), |
| 171 | + length(UVs, U), |
| 172 | + random_permutation(Bindings, Shuffled), |
| 173 | + length(Shuffled, N), |
| 174 | + random_between(0, N, K), |
| 175 | + length(Prefix, K), |
| 176 | + append(Prefix, _Rest, Shuffled), |
| 177 | + plan_prefix_vars(Prefix, PrefixVs), |
| 178 | + length(PrefixVs, PU), |
| 179 | + ( K > 0, maybe(0.5) |
| 180 | + -> random_between(1, K, Idx), |
| 181 | + nth1(Idx, Prefix, V=Val), |
| 182 | + clash_value(Val, Clash), |
| 183 | + nth1_replace(Idx, Prefix, V=Clash, Plan), |
| 184 | + Expected = ok |
| 185 | + ; Plan = Prefix, |
| 186 | + ( PU =:= U |
| 187 | + -> Expected = fail % covers all unique vars |
| 188 | + ; Expected = ok |
| 189 | + ) |
| 190 | + ). |
| 191 | + |
| 192 | +unique_vars(Bindings, UVs) :- |
| 193 | + findall(V, member(V=_, Bindings), Vs), |
| 194 | + sort(Vs, UVs). |
| 195 | + |
| 196 | +plan_prefix_vars(Prefix, PVs) :- |
| 197 | + findall(V, member(V=_, Prefix), Vs), |
| 198 | + sort(Vs, PVs). |
| 199 | + |
| 200 | +nth1_replace(1, [_|T], X, [X|T]) :- !. |
| 201 | +nth1_replace(N, [H|T], X, [H|R]) :- |
| 202 | + N1 is N - 1, |
| 203 | + nth1_replace(N1, T, X, R). |
| 204 | + |
| 205 | +%! clash_value(+Val, -Clash) is det. |
| 206 | +% |
| 207 | +% Produce a value guaranteed not to unify with Val given the leaf |
| 208 | +% domains random_term uses (atoms `[a,b,c]`, ints in `-100..100`). |
| 209 | + |
| 210 | +clash_value(Val, Clash) :- |
| 211 | + ( integer(Val) |
| 212 | + -> Clash is Val + 10_000 |
| 213 | + ; atom(Val) |
| 214 | + -> atom_concat(clashed_, Val, Clash) |
| 215 | + ; compound(Val) |
| 216 | + -> Clash = clashed_atom |
| 217 | + ; Clash = clashed_atom |
| 218 | + ). |
| 219 | + |
| 220 | +%! run(+G, +T, +Plan, -Observed) is det. |
| 221 | +% |
| 222 | +% Post dif(G, T) and apply the plan. Observed is `ok` when the whole |
| 223 | +% sequence succeeded, `fail` when it failed, `error(E)` on exception. |
| 224 | + |
| 225 | +run(G, T, Plan, Observed) :- |
| 226 | + catch( |
| 227 | + ( ( dif(G, T), apply_plan(Plan) |
| 228 | + -> Observed = ok |
| 229 | + ; Observed = fail |
| 230 | + ) |
| 231 | + ), |
| 232 | + Error, |
| 233 | + Observed = error(Error) |
| 234 | + ). |
| 235 | + |
| 236 | +apply_plan([]). |
| 237 | +apply_plan([V=X|T]) :- |
| 238 | + V = X, |
| 239 | + apply_plan(T). |
| 240 | + |
| 241 | +report(Phase, T, G, Plan, Expected, Observed) :- |
| 242 | + copy_term(T-G-Plan, Tc-Gc-Pc), |
| 243 | + numbervars(Tc-Gc-Pc, 0, _, [singletons(false)]), |
| 244 | + format(user_error, |
| 245 | + "MISMATCH ~w: expected ~w, observed ~w~n T=~q~n G=~q~n Plan=~q~n", |
| 246 | + [Phase, Expected, Observed, Tc, Gc, Pc]). |
0 commit comments