eyeprolog 1.5.92 → 1.5.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/test/README.md +19 -0
- package/test/run-all.mjs +2 -0
- package/test/run-properties.mjs +295 -0
package/package.json
CHANGED
package/test/README.md
CHANGED
|
@@ -17,6 +17,7 @@ node test/run-neumerkel-tests.mjs # upstream-fetch harness
|
|
|
17
17
|
node test/run-examples.mjs
|
|
18
18
|
node test/run-playground.mjs
|
|
19
19
|
node test/run-architecture.mjs
|
|
20
|
+
node test/run-properties.mjs # seeded random-term invariant checks (see below)
|
|
20
21
|
node test/run-openrulebench.mjs
|
|
21
22
|
node test/run-http-json.mjs
|
|
22
23
|
node test/run-interop.mjs # requires the comparison engines
|
|
@@ -26,6 +27,24 @@ node test/run-benchmark-tests.mjs # benchmark harness
|
|
|
26
27
|
These runners retain their existing options; there is no separate npm alias for
|
|
27
28
|
each one. Focused checks do not replace the full release gate.
|
|
28
29
|
|
|
30
|
+
`run-properties.mjs` is different in kind from the rest of the suite: instead
|
|
31
|
+
of hand-picked inputs, it generates many random ground terms from a seeded
|
|
32
|
+
PRNG and checks invariants that must hold for any input -- write/read
|
|
33
|
+
round-tripping, `sort/2` ordering and idempotence, `append/3`/`length/2`
|
|
34
|
+
agreement, `compare/3` symmetry, `keysort/2` stability, `=..` round-tripping,
|
|
35
|
+
`copy_term/2` sharing, `reverse/2` involution, `atom_codes/2`/`atom_chars/2`/
|
|
36
|
+
`char_code/2`/`number_codes/2` round-tripping, `succ/2` and `abs/1`/`sign/1`
|
|
37
|
+
arithmetic identities, `nth0/3`/`nth1/3` agreement, `last/2`, `min_list/2`/
|
|
38
|
+
`max_list/2` bounds, `sum_list/2` additivity, `atom_concat/3`/`string_concat/3`
|
|
39
|
+
regrouping, `atom_string/2`/`number_string/2` round-tripping, `list_to_set/2`
|
|
40
|
+
idempotence, `permutation/2`, and `between/3` range generation -- roughly 25
|
|
41
|
+
properties, each reported as its own trial (currently ~390 individual test
|
|
42
|
+
lines) so a failure names exactly which trial of which property broke rather
|
|
43
|
+
than leaving a reader to dig through one aggregate error. The whole file
|
|
44
|
+
still runs in under two seconds. The seed (`SEED` in that file) is fixed so a
|
|
45
|
+
failure is exactly reproducible by rerunning it -- change it only
|
|
46
|
+
deliberately, and say why, never to make a transient failure disappear.
|
|
47
|
+
|
|
29
48
|
For performance measurements, use `npm run benchmark`. Save a local baseline with
|
|
30
49
|
`npm run benchmark -- --save .benchmarks/baseline.json`.
|
|
31
50
|
|
package/test/run-all.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { runExamples } from './run-examples.mjs';
|
|
|
12
12
|
import { runBookExamples } from './run-book-examples.mjs';
|
|
13
13
|
import { runOpenRuleBenchChecks } from './run-openrulebench.mjs';
|
|
14
14
|
import { runArchitecture } from './run-architecture.mjs';
|
|
15
|
+
import { runProperties } from './run-properties.mjs';
|
|
15
16
|
import { runCleanup } from './run-cleanup.mjs';
|
|
16
17
|
import { runHttpJson } from './run-http-json.mjs';
|
|
17
18
|
import { runNeumerkel } from './run-neumerkel.mjs';
|
|
@@ -27,6 +28,7 @@ await runStandalone(async (reporter) => {
|
|
|
27
28
|
runIsoPart2Amendment(reporter);
|
|
28
29
|
runOpenRuleBenchChecks(reporter);
|
|
29
30
|
runArchitecture(reporter);
|
|
31
|
+
runProperties(reporter);
|
|
30
32
|
runCleanup(reporter);
|
|
31
33
|
await runHttpJson(reporter);
|
|
32
34
|
await runRegression(reporter);
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Property-based / round-trip regression tests.
|
|
3
|
+
//
|
|
4
|
+
// Every other conformance and regression case in this suite is a single,
|
|
5
|
+
// hand-picked input: a human decided which term, which list, which pair of
|
|
6
|
+
// operators was worth checking. That is precise, but it only ever catches
|
|
7
|
+
// what someone already thought to write down. This file instead generates
|
|
8
|
+
// many random ground terms from a seeded PRNG and checks invariants that
|
|
9
|
+
// must hold for ANY input, not just the ones on record -- the kind of edge
|
|
10
|
+
// case (an empty atom, an embedded quote, a duplicate sort key, a term
|
|
11
|
+
// whose functor happens to collide with an operator) that a hand-written
|
|
12
|
+
// corpus tends to miss by omission rather than by design.
|
|
13
|
+
//
|
|
14
|
+
// Each trial is its own reporter.test() entry, the same reason the Neumerkel
|
|
15
|
+
// gate reports every answer description as its own test (see
|
|
16
|
+
// test/neumerkel.mjs): a single aggregate pass/fail line for a whole
|
|
17
|
+
// property would force a reader to dig through a raw error string to find
|
|
18
|
+
// which of many trials actually broke, instead of seeing it named outright.
|
|
19
|
+
//
|
|
20
|
+
// The seed is fixed, not time-based: a failure must be exactly reproducible
|
|
21
|
+
// by re-running this file, not a flake that vanishes on the next run. Only
|
|
22
|
+
// change SEED deliberately (and say why in the commit), never to make a
|
|
23
|
+
// failure go away. Trial counts are tuned to keep the whole file in the
|
|
24
|
+
// 10-20 second range as part of `npm test`; if a new property needs more
|
|
25
|
+
// trials to be meaningful, trim another property's count to make room
|
|
26
|
+
// rather than letting the total creep up unnoticed.
|
|
27
|
+
import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
|
|
28
|
+
import { run } from '../src/index.js';
|
|
29
|
+
|
|
30
|
+
const SEED = 20260913;
|
|
31
|
+
const MAX_DEPTH = 3;
|
|
32
|
+
|
|
33
|
+
// mulberry32: a small, fast, deterministic PRNG. Good enough for generating
|
|
34
|
+
// test inputs -- this is not a security context -- and its whole point here
|
|
35
|
+
// is determinism, which Math.random() cannot offer.
|
|
36
|
+
function mulberry32(seed) {
|
|
37
|
+
let a = seed >>> 0;
|
|
38
|
+
return function next() {
|
|
39
|
+
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
|
40
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
41
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
42
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// One PRNG stream, shared across every property in file order, so the
|
|
47
|
+
// overall run is still governed by the single SEED at the top: rerunning
|
|
48
|
+
// this file reruns the exact same sequence of generated inputs everywhere,
|
|
49
|
+
// not just within one property.
|
|
50
|
+
const rng = mulberry32(SEED);
|
|
51
|
+
|
|
52
|
+
function pick(list) { return list[Math.floor(rng() * list.length)]; }
|
|
53
|
+
function int(lo, hi) { return lo + Math.floor(rng() * (hi - lo + 1)); }
|
|
54
|
+
|
|
55
|
+
// Atoms deliberately include the awkward cases a hand-written corpus tends
|
|
56
|
+
// to skip: the empty atom, an embedded quote, digits-first, and an atom
|
|
57
|
+
// that is itself a list-notation special form.
|
|
58
|
+
const ATOM_POOL = [
|
|
59
|
+
'a', 'b', 'c', 'foo', 'bar', 'baz',
|
|
60
|
+
"''", "'A weird atom'", "'with''quote'", "'123abc'", "'[]'", "'{}'",
|
|
61
|
+
];
|
|
62
|
+
// A separate, narrower pool for properties that build/compare atom *text*
|
|
63
|
+
// (atom_concat, string_concat, atom_codes/chars): plain lowercase words only,
|
|
64
|
+
// so concatenation and character decomposition stay easy to reason about
|
|
65
|
+
// without also re-deriving quoting rules inside the property itself.
|
|
66
|
+
const WORD_POOL = ['a', 'b', 'c', 'foo', 'bar', 'baz', 'quux', 'x', 'yz', 'hello'];
|
|
67
|
+
const FUNCTOR_POOL = ['f', 'g', 'h', 'node', 'pair', 'wrap'];
|
|
68
|
+
|
|
69
|
+
function genAtomic() {
|
|
70
|
+
switch (pick(['atom', 'int', 'negint', 'bigint', 'float'])) {
|
|
71
|
+
case 'atom': return pick(ATOM_POOL);
|
|
72
|
+
case 'int': return String(int(0, 1000));
|
|
73
|
+
case 'negint': return String(-int(1, 1000));
|
|
74
|
+
case 'bigint': {
|
|
75
|
+
let digits = String(int(1, 9));
|
|
76
|
+
for (let i = 0; i < int(20, 40); i++) digits += int(0, 9);
|
|
77
|
+
return digits;
|
|
78
|
+
}
|
|
79
|
+
case 'float': {
|
|
80
|
+
const sign = rng() < 0.5 ? '-' : '';
|
|
81
|
+
return `${sign}${int(0, 999)}.${String(int(0, 999)).padStart(3, '0')}`;
|
|
82
|
+
}
|
|
83
|
+
default: throw new Error('unreachable');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function genTerm(depth = MAX_DEPTH) {
|
|
88
|
+
if (depth <= 0 || rng() < 0.4) return genAtomic();
|
|
89
|
+
if (rng() < 0.5) {
|
|
90
|
+
const items = Array.from({ length: int(0, 4) }, () => genTerm(depth - 1));
|
|
91
|
+
return `[${items.join(', ')}]`;
|
|
92
|
+
}
|
|
93
|
+
const args = Array.from({ length: int(1, 3) }, () => genTerm(depth - 1));
|
|
94
|
+
return `${pick(FUNCTOR_POOL)}(${args.join(', ')})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function genCompound() {
|
|
98
|
+
// Force a compound (never atomic) so univ round-tripping is meaningful.
|
|
99
|
+
const args = Array.from({ length: int(1, 4) }, () => genTerm(MAX_DEPTH - 1));
|
|
100
|
+
return `${pick(FUNCTOR_POOL)}(${args.join(', ')})`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function genList(depth = 1) { return `[${Array.from({ length: int(0, 6) }, () => genTerm(depth)).join(', ')}]`; }
|
|
104
|
+
function genWord() { return pick(WORD_POOL); }
|
|
105
|
+
function genSmallInt() { return int(0, 1000); }
|
|
106
|
+
function genNonZeroInt() { const n = int(1, 1000); return rng() < 0.5 ? -n : n; }
|
|
107
|
+
|
|
108
|
+
// Runs one goal and confirms it actually succeeded, throwing with the goal
|
|
109
|
+
// text attached (so a failure names exactly what to rerun) otherwise.
|
|
110
|
+
//
|
|
111
|
+
// stats.completed_goal_lists is the one reliable success signal, not the
|
|
112
|
+
// printed answer text: a goal left with unbound variables (routine here,
|
|
113
|
+
// since `\+` discards the bindings it made) prints nothing at all despite
|
|
114
|
+
// succeeding, and a ground goal that succeeds is echoed back in full,
|
|
115
|
+
// untaken branches included -- so a literal marker atom anywhere in the
|
|
116
|
+
// goal text can appear in that echo without ever having been reached.
|
|
117
|
+
function solve(goal, { use = [] } = {}) {
|
|
118
|
+
const prelude = use.map((lib) => `:- use_module(library(${lib})).\n`).join('');
|
|
119
|
+
const result = run(prelude, { goal });
|
|
120
|
+
if (result.stats.completed_goal_lists < 1) {
|
|
121
|
+
throw new Error(`property goal did not hold (failed or errored): ${goal}`);
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Registers `trials` individually-reported trials of one property. genFn()
|
|
127
|
+
// builds a fresh random goal (as a string) each time; it alone decides what
|
|
128
|
+
// varies between trials, so it is called with no arguments (the shared
|
|
129
|
+
// module-level `rng` is what actually advances).
|
|
130
|
+
function property(reporter, name, trials, genFn, options) {
|
|
131
|
+
for (let trial = 0; trial < trials; trial++) {
|
|
132
|
+
reporter.test(`${name} (trial ${trial + 1}/${trials})`, () => {
|
|
133
|
+
const goal = genFn();
|
|
134
|
+
try {
|
|
135
|
+
solve(goal, options);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(`${name}: trial ${trial} (seed ${SEED}) failed -- ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function runProperties(reporter = new TestReporter()) {
|
|
144
|
+
reporter.section('Property-based round-trip checks');
|
|
145
|
+
|
|
146
|
+
property(reporter, 'write_term_to_chars / read_term_from_chars round-trips any ground term', 20, () =>
|
|
147
|
+
`T = (${genTerm()}), write_term_to_chars(T, [quoted(true)], C0), ` +
|
|
148
|
+
`append(C0, ".", C), read_term_from_chars(C, T2, []), T == T2`,
|
|
149
|
+
{ use: ['charsio'] });
|
|
150
|
+
|
|
151
|
+
property(reporter, 'sort/2 is idempotent, ordered, and duplicate-free', 20, () =>
|
|
152
|
+
// No adjacent pair may fail to be strictly increasing: sort/2 removes
|
|
153
|
+
// duplicates per ISO 8.4.3, so a non-strict (@>=) neighbor is a bug.
|
|
154
|
+
// append/3 enumerating every split is plain ISO core, so this needs no
|
|
155
|
+
// recursive helper predicate of its own.
|
|
156
|
+
`L = ${genList()}, sort(L, S1), sort(S1, S2), S1 == S2, ` +
|
|
157
|
+
`\\+ (append(_, [X, Y|_], S1), X @>= Y)`);
|
|
158
|
+
|
|
159
|
+
property(reporter, 'append/3 and length/2 agree on the combined length', 20, () =>
|
|
160
|
+
`A = ${genList()}, B = ${genList()}, append(A, B, C), length(A, LA), length(B, LB), length(C, LC), ` +
|
|
161
|
+
`LC =:= LA + LB, ` +
|
|
162
|
+
// append/3's "split" mode must invert cleanly: recovering A from C at
|
|
163
|
+
// the same split point must reproduce the same B.
|
|
164
|
+
`append(A, B2, C), B2 == B`);
|
|
165
|
+
|
|
166
|
+
property(reporter, 'compare/3 gives mirrored results for swapped arguments', 15, () =>
|
|
167
|
+
`X = (${genTerm(2)}), Y = (${genTerm(2)}), compare(O1, X, Y), compare(O2, Y, X), ` +
|
|
168
|
+
`( O1 == (<) -> O2 == (>) ; O1 == (>) -> O2 == (<) ; O1 == (=), O2 == (=) )`);
|
|
169
|
+
|
|
170
|
+
property(reporter, 'keysort/2 is stable across duplicate keys', 20, () => {
|
|
171
|
+
// Keys drawn from a small range to force duplicates; each value is the
|
|
172
|
+
// pair's original position, so stability can be checked directly:
|
|
173
|
+
// among equal keys, positions must stay in ascending (original) order.
|
|
174
|
+
const pairs = Array.from({ length: int(2, 10) }, (_, index) => `${int(0, 3)}-${index}`);
|
|
175
|
+
// No adjacent pair may go backward: keys must never decrease, and where
|
|
176
|
+
// two keys tie, the original index (the value half) must not decrease
|
|
177
|
+
// either -- that is exactly what "stable" means.
|
|
178
|
+
return `L = [${pairs.join(', ')}], keysort(L, S), ` +
|
|
179
|
+
`\\+ (append(_, [K1-V1, K2-V2|_], S), (K1 @> K2 ; K1 == K2, V1 > V2))`;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
property(reporter, 'univ (=..) round-trips any compound term', 15, () =>
|
|
183
|
+
`T = (${genCompound()}), T =.. L, T3 =.. L, T3 == T`);
|
|
184
|
+
|
|
185
|
+
property(reporter, 'copy_term/2 preserves variable sharing without aliasing the original', 20, () => {
|
|
186
|
+
// Alternate sharing shapes: (shared, shared, fresh), (fresh, shared, shared).
|
|
187
|
+
const filler = genAtomic();
|
|
188
|
+
const template = pick([
|
|
189
|
+
`f(X, X, ${filler})`,
|
|
190
|
+
`g(${filler}, X, X)`,
|
|
191
|
+
`h(X, ${filler}, X)`,
|
|
192
|
+
]);
|
|
193
|
+
return `T = ${template}, copy_term(T, C), variant(T, C), ` +
|
|
194
|
+
// The two positions that shared a variable in T must still be
|
|
195
|
+
// identical to each other in the copy (structure preserved) ...
|
|
196
|
+
`T =.. [_, TA, TB, TC], C =.. [_, CA, CB, CC], ` +
|
|
197
|
+
`( (TA == TB, TA \\== ${filler}) -> CA == CB ; true ), ` +
|
|
198
|
+
`( (TB == TC, TB \\== ${filler}) -> CB == CC ; true ), ` +
|
|
199
|
+
`( (TA == TC, TA \\== ${filler}) -> CA == CC ; true )`;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
property(reporter, 'reverse/2 is its own inverse', 15, () =>
|
|
203
|
+
`L = ${genList()}, reverse(L, R), reverse(R, L2), L == L2`);
|
|
204
|
+
|
|
205
|
+
property(reporter, 'atom_codes/2 round-trips any atom', 15, () =>
|
|
206
|
+
`atom_codes(${genWord()}, C0), atom_codes(A2, C0), atom_codes(A2, C1), C0 == C1`);
|
|
207
|
+
|
|
208
|
+
property(reporter, 'atom_chars/2 round-trips any atom', 15, () =>
|
|
209
|
+
`atom_chars(${genWord()}, C0), atom_chars(A2, C0), atom_chars(A2, C1), C0 == C1`);
|
|
210
|
+
|
|
211
|
+
property(reporter, 'char_code/2 round-trips any character', 15, () => {
|
|
212
|
+
const code = int(32, 126); // printable ASCII, safe to embed as a bare integer
|
|
213
|
+
return `char_code(Ch, ${code}), char_code(Ch2, ${code}), Ch == Ch2`;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
property(reporter, 'number_codes/2 round-trips any integer', 15, () =>
|
|
217
|
+
`number_codes(${genNonZeroInt()}, C), number_codes(N, C), number_codes(N, C2), C == C2`);
|
|
218
|
+
|
|
219
|
+
property(reporter, 'succ/2 agrees with +1', 15, () => {
|
|
220
|
+
const n = int(0, 100000);
|
|
221
|
+
return `succ(${n}, S), S =:= ${n} + 1, succ(P, S), P =:= ${n}`;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
property(reporter, 'abs/1 and sign/1 reconstruct the original integer', 15, () => {
|
|
225
|
+
const n = genNonZeroInt();
|
|
226
|
+
return `X is ${n}, A is abs(X), A >= 0, S is sign(X), V is S * A, V =:= X`;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
property(reporter, 'nth0/3 and nth1/3 agree on the same element', 15, () => {
|
|
230
|
+
const list = `[${Array.from({ length: int(1, 6) }, () => genTerm(1)).join(', ')}]`;
|
|
231
|
+
const pick0 = int(0, 100); // reduced mod the list's actual length below
|
|
232
|
+
return `L = ${list}, length(L, Len), I is ${pick0} mod Len, ` +
|
|
233
|
+
`nth0(I, L, X), I1 is I + 1, nth1(I1, L, X2), X == X2`;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
property(reporter, 'last/2 names the element append/3 would split off', 15, () => {
|
|
237
|
+
const list = `[${Array.from({ length: int(1, 6) }, () => genTerm(1)).join(', ')}]`;
|
|
238
|
+
return `L = ${list}, last(L, X), append(_, [X], L)`;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
property(reporter, 'min_list/2 and max_list/2 bound every element', 15, () => {
|
|
242
|
+
const list = `[${Array.from({ length: int(1, 8) }, () => genNonZeroInt()).join(', ')}]`;
|
|
243
|
+
return `L = ${list}, min_list(L, Mn), max_list(L, Mx), \\+ (member(E, L), (E < Mn ; E > Mx))`;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
property(reporter, 'sum_list/2 is additive under append/3', 15, () => {
|
|
247
|
+
const a = `[${Array.from({ length: int(0, 5) }, () => genSmallInt()).join(', ')}]`;
|
|
248
|
+
const b = `[${Array.from({ length: int(0, 5) }, () => genSmallInt()).join(', ')}]`;
|
|
249
|
+
return `A = ${a}, B = ${b}, sum_list(A, SA), sum_list(B, SB), append(A, B, C), sum_list(C, SC), ` +
|
|
250
|
+
`SC =:= SA + SB`;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// atom_concat/3 associativity needs the SAME three words on both sides of
|
|
254
|
+
// the regrouping -- three independent genWord() calls per side would pick
|
|
255
|
+
// different words each time and prove nothing.
|
|
256
|
+
property(reporter, 'atom_concat/3 regroups the same three atoms consistently', 15, () => {
|
|
257
|
+
const [w1, w2, w3] = [genWord(), genWord(), genWord()];
|
|
258
|
+
return `atom_concat(${w1}, ${w2}, AB), atom_concat(AB, ${w3}, ABC1), ` +
|
|
259
|
+
`atom_concat(${w2}, ${w3}, BC), atom_concat(${w1}, BC, ABC2), ABC1 == ABC2`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
property(reporter, 'string_concat/3 regroups the same three atoms consistently', 15, () => {
|
|
263
|
+
const [w1, w2, w3] = [genWord(), genWord(), genWord()];
|
|
264
|
+
return `string_concat(${w1}, ${w2}, AB), string_concat(AB, ${w3}, ABC1), ` +
|
|
265
|
+
`string_concat(${w2}, ${w3}, BC), string_concat(${w1}, BC, ABC2), ABC1 == ABC2`;
|
|
266
|
+
}, { use: ['strings'] });
|
|
267
|
+
|
|
268
|
+
property(reporter, 'atom_string/2 round-trips any atom', 12, () =>
|
|
269
|
+
`atom_string(${genWord()}, S), atom_string(A2, S), atom_string(A2, S2), S == S2`,
|
|
270
|
+
{ use: ['strings'] });
|
|
271
|
+
|
|
272
|
+
property(reporter, 'number_string/2 round-trips any integer', 12, () =>
|
|
273
|
+
`number_string(${genNonZeroInt()}, S), number_string(N, S), number_string(N, S2), S == S2`,
|
|
274
|
+
{ use: ['strings'] });
|
|
275
|
+
|
|
276
|
+
property(reporter, 'list_to_set/2 is idempotent', 15, () =>
|
|
277
|
+
`L = ${genList()}, list_to_set(L, S1), list_to_set(S1, S2), S1 == S2`,
|
|
278
|
+
{ use: ['lists'] });
|
|
279
|
+
|
|
280
|
+
property(reporter, 'permutation/2 preserves length and distinct-element order', 12, () => {
|
|
281
|
+
const list = `[${Array.from({ length: int(0, 5) }, () => genTerm(1)).join(', ')}]`;
|
|
282
|
+
return `L = ${list}, once(permutation(L, P)), length(L, N), length(P, N), sort(L, SL), sort(P, SP), SL == SP`;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
property(reporter, 'between/3 generates exactly the requested inclusive range', 10, () => {
|
|
286
|
+
const lo = int(-5, 5);
|
|
287
|
+
const hi = lo + int(0, 8);
|
|
288
|
+
return `findall(X, between(${lo}, ${hi}, X), L), length(L, N), N =:= ${hi} - ${lo} + 1, ` +
|
|
289
|
+
`nth0(0, L, F), F =:= ${lo}, last(L, La), La =:= ${hi}`;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
reporter.sectionTotal('property-based round-trip checks');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (isMainModule(import.meta.url)) await runStandalone(runProperties);
|