eyeprolog 1.2.13 → 1.2.14
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/src/iso.js +14 -7
- package/src/solver.js +1 -1
- package/src/term.js +28 -19
- package/test/conformance/cases/iso/grouped_solutions_and_clauses.pl +6 -2
- package/test/conformance/expected/iso/grouped_solutions_and_clauses.pl +1 -1
- package/test/run-regression.mjs +21 -0
- package/the-art-of-eyeprolog.md +13 -3
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -284,8 +284,8 @@ function termIsAcyclic(term, env) {
|
|
|
284
284
|
return true;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
function resolvedOrder(left, right, env) {
|
|
288
|
-
return compareTerms(copyResolved(left, env), copyResolved(right, env));
|
|
287
|
+
function resolvedOrder(left, right, env, variableRanks = null) {
|
|
288
|
+
return compareTerms(copyResolved(left, env), copyResolved(right, env), variableRanks);
|
|
289
289
|
}
|
|
290
290
|
function* compareBuiltin({ goal, env }) {
|
|
291
291
|
const order = deref(goal.args[0], env);
|
|
@@ -331,10 +331,11 @@ function validateListOutput(term, env) {
|
|
|
331
331
|
function* sortBuiltin({ goal, env }) {
|
|
332
332
|
const items = requireProperList(goal.args[0], env);
|
|
333
333
|
validateListOutput(goal.args[1], env);
|
|
334
|
-
const
|
|
334
|
+
const variableRanks = new Map();
|
|
335
|
+
const sorted = [...items].sort((a, b) => resolvedOrder(a, b, env, variableRanks));
|
|
335
336
|
const unique = [];
|
|
336
337
|
for (const item of sorted) {
|
|
337
|
-
if (unique.length === 0 || resolvedOrder(unique[unique.length - 1], item, env) !== 0) unique.push(item);
|
|
338
|
+
if (unique.length === 0 || resolvedOrder(unique[unique.length - 1], item, env, variableRanks) !== 0) unique.push(item);
|
|
338
339
|
}
|
|
339
340
|
const next = env.clone();
|
|
340
341
|
if (unify(goal.args[1], listFromItems(unique), next)) yield next;
|
|
@@ -350,7 +351,11 @@ function* keysortBuiltin({ goal, env }) {
|
|
|
350
351
|
}
|
|
351
352
|
}
|
|
352
353
|
// Modern ECMAScript specifies a stable Array#sort, as required by keysort/2.
|
|
353
|
-
|
|
354
|
+
// Keep one implementation-dependent variable order for this whole sorting
|
|
355
|
+
// operation, as required by ISO 7.2.1.
|
|
356
|
+
const variableRanks = new Map();
|
|
357
|
+
const sorted = [...items].sort((a, b) =>
|
|
358
|
+
resolvedOrder(deref(a, env).args[0], deref(b, env).args[0], env, variableRanks));
|
|
354
359
|
const next = env.clone();
|
|
355
360
|
if (unify(goal.args[1], listFromItems(sorted), next)) yield next;
|
|
356
361
|
}
|
|
@@ -1701,8 +1706,10 @@ function sameWitness(left, right) {
|
|
|
1701
1706
|
}
|
|
1702
1707
|
|
|
1703
1708
|
function sortedUnique(items) {
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1709
|
+
const variableRanks = new Map();
|
|
1710
|
+
const compare = (left, right) => compareTerms(left, right, variableRanks);
|
|
1711
|
+
const sorted = [...items].sort(compare);
|
|
1712
|
+
return sorted.filter((item, index) => index === 0 || compare(sorted[index - 1], item) !== 0);
|
|
1706
1713
|
}
|
|
1707
1714
|
|
|
1708
1715
|
function allSolutionsBuiltin(asSet) {
|
package/src/solver.js
CHANGED
|
@@ -1250,7 +1250,7 @@ function copyResolvedWithKey(term, env, variables) {
|
|
|
1250
1250
|
id = variables.size;
|
|
1251
1251
|
variables.set(value.name, id);
|
|
1252
1252
|
}
|
|
1253
|
-
return { term: termModuleCache.variable(value.name
|
|
1253
|
+
return { term: termModuleCache.variable(value.name), key: `var:${id}` };
|
|
1254
1254
|
}
|
|
1255
1255
|
if (!value.args?.length) {
|
|
1256
1256
|
return {
|
package/src/term.js
CHANGED
|
@@ -6,10 +6,6 @@ export const STRING = 'string';
|
|
|
6
6
|
export const NUMBER = 'number';
|
|
7
7
|
export const COMPOUND = 'compound';
|
|
8
8
|
const EMPTY_ARGS = Object.freeze([]);
|
|
9
|
-
// Variable term order is carried by the variable objects themselves. Do not
|
|
10
|
-
// retain a process-global name registry: runtime clause freshening can create
|
|
11
|
-
// an unbounded sequence of names in an otherwise constant-space computation.
|
|
12
|
-
let variableOrder = 0;
|
|
13
9
|
|
|
14
10
|
export class Term {
|
|
15
11
|
constructor(type, name, args = []) {
|
|
@@ -22,11 +18,7 @@ export class Term {
|
|
|
22
18
|
}
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
export const variable = (name
|
|
26
|
-
const term = new Term(VAR, name, EMPTY_ARGS);
|
|
27
|
-
term.order = order ?? ++variableOrder;
|
|
28
|
-
return term;
|
|
29
|
-
};
|
|
21
|
+
export const variable = (name) => new Term(VAR, name, EMPTY_ARGS);
|
|
30
22
|
export const atom = (name) => new Term(ATOM, name, EMPTY_ARGS);
|
|
31
23
|
export const stringTerm = (value) => new Term(STRING, value, EMPTY_ARGS);
|
|
32
24
|
export const numberTerm = (value) => new Term(NUMBER, value, EMPTY_ARGS);
|
|
@@ -271,7 +263,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
271
263
|
}
|
|
272
264
|
|
|
273
265
|
export function cloneTerm(term) {
|
|
274
|
-
if (term.type === VAR) return variable(term.name
|
|
266
|
+
if (term.type === VAR) return variable(term.name);
|
|
275
267
|
const cloned = term.type === COMPOUND && term.arity === 0
|
|
276
268
|
? atom(term.name)
|
|
277
269
|
: new Term(term.type, term.name, term.args.map(cloneTerm));
|
|
@@ -297,7 +289,7 @@ export function freshTerm(term, suffix, variables = new Map()) {
|
|
|
297
289
|
|
|
298
290
|
export function copyResolved(term, env) {
|
|
299
291
|
const resolved = deref(term, env);
|
|
300
|
-
if (resolved.type === VAR) return variable(resolved.name
|
|
292
|
+
if (resolved.type === VAR) return variable(resolved.name);
|
|
301
293
|
const copied = resolved.type === COMPOUND && resolved.arity === 0
|
|
302
294
|
? atom(resolved.name)
|
|
303
295
|
: new Term(resolved.type, resolved.name, resolved.args.map((arg) => copyResolved(arg, env)));
|
|
@@ -512,7 +504,27 @@ export function variantTerms(left, leftEnv, right, rightEnv, pairs = new Map(),
|
|
|
512
504
|
return true;
|
|
513
505
|
}
|
|
514
506
|
|
|
515
|
-
export function compareTerms(left, right) {
|
|
507
|
+
export function compareTerms(left, right, variableRanks = null) {
|
|
508
|
+
// ISO 7.2.1 deliberately leaves the order of distinct variables
|
|
509
|
+
// implementation dependent. Do not attach a permanent ordinal to a
|
|
510
|
+
// logical variable: besides retaining implementation history, that would
|
|
511
|
+
// make the chosen order observable outside the operation that needs it.
|
|
512
|
+
// A caller that is constructing one sorted list can pass a shared Map so
|
|
513
|
+
// every comparison in that operation uses one consistent variable order.
|
|
514
|
+
const ranks = variableRanks ?? new Map();
|
|
515
|
+
return compareTermsWithRanks(left, right, ranks);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function variableRank(name, ranks) {
|
|
519
|
+
let rank = ranks.get(name);
|
|
520
|
+
if (rank == null) {
|
|
521
|
+
rank = ranks.size;
|
|
522
|
+
ranks.set(name, rank);
|
|
523
|
+
}
|
|
524
|
+
return rank;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function compareTermsWithRanks(left, right, variableRanks) {
|
|
516
528
|
const rank = (term) => ({ [VAR]: 0, [NUMBER]: 1, [ATOM]: 2, [STRING]: 3, [COMPOUND]: 4 })[term.type];
|
|
517
529
|
left = deref(left, new Env());
|
|
518
530
|
right = deref(right, new Env());
|
|
@@ -527,18 +539,15 @@ export function compareTerms(left, right) {
|
|
|
527
539
|
}
|
|
528
540
|
if (left.type === VAR) {
|
|
529
541
|
if (left.name === right.name) return 0;
|
|
530
|
-
const leftOrder = left.
|
|
531
|
-
const rightOrder = right.
|
|
532
|
-
|
|
533
|
-
// Explicitly copied order values are permitted internally; keep the
|
|
534
|
-
// implementation-defined variable order total even in that case.
|
|
535
|
-
return left.name < right.name ? -1 : 1;
|
|
542
|
+
const leftOrder = variableRank(left.name, variableRanks);
|
|
543
|
+
const rightOrder = variableRank(right.name, variableRanks);
|
|
544
|
+
return leftOrder < rightOrder ? -1 : 1;
|
|
536
545
|
}
|
|
537
546
|
if (left.type === ATOM || left.type === STRING) return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
538
547
|
if (left.arity !== right.arity) return left.arity < right.arity ? -1 : 1;
|
|
539
548
|
if (left.name !== right.name) return left.name < right.name ? -1 : 1;
|
|
540
549
|
for (let i = 0; i < left.arity; i++) {
|
|
541
|
-
const cmp =
|
|
550
|
+
const cmp = compareTermsWithRanks(left.args[i], right.args[i], variableRanks);
|
|
542
551
|
if (cmp) return cmp;
|
|
543
552
|
}
|
|
544
553
|
return 0;
|
|
@@ -45,7 +45,11 @@ shared_clause(Body) :-
|
|
|
45
45
|
|
|
46
46
|
%% goal: shared_set_variables(X0)
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
% ISO 7.2.1 leaves the order of distinct variables implementation dependent.
|
|
49
|
+
% Check that setof/3 constructs one consistent sorted list without baking a
|
|
50
|
+
% particular variable order into the conformance golden.
|
|
51
|
+
shared_set_variables(ok) :-
|
|
49
52
|
setof(Value, (=(Value, Left); =(Value, Right)), Set),
|
|
50
53
|
=(Left, a),
|
|
51
|
-
=(Right, b)
|
|
54
|
+
=(Right, b),
|
|
55
|
+
(=(Set, [a,b]); =(Set, [b,a])).
|
package/test/run-regression.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
parseProgramText,
|
|
40
40
|
} from '../src/index.js';
|
|
41
41
|
import { parseGoalText } from '../src/parser.js';
|
|
42
|
+
import { compareTerms } from '../src/term.js';
|
|
42
43
|
import { selectClauseCandidates } from '../src/program.js';
|
|
43
44
|
import { TestReporter, isMainModule } from './test-style.mjs';
|
|
44
45
|
import { buildConformanceReport, formatConformanceReport } from './run-conformance-report.mjs';
|
|
@@ -2443,6 +2444,26 @@ function whiteBoxCases() {
|
|
|
2443
2444
|
assertEqual(termToString(variable('X'), env, true), 'socrates', 'binding');
|
|
2444
2445
|
},
|
|
2445
2446
|
},
|
|
2447
|
+
{
|
|
2448
|
+
name: 'variable term order is scoped to one comparison or sorted-list operation',
|
|
2449
|
+
run: () => {
|
|
2450
|
+
const left = variable('Left');
|
|
2451
|
+
const right = variable('Right');
|
|
2452
|
+
assertEqual(String(left.order), 'undefined', 'variables carry no persistent order ordinal');
|
|
2453
|
+
assertEqual(String(right.order), 'undefined', 'second variable carries no persistent order ordinal');
|
|
2454
|
+
|
|
2455
|
+
// Separate comparisons are permitted to choose their own
|
|
2456
|
+
// implementation-dependent order under ISO 7.2.1.
|
|
2457
|
+
assertEqual(String(compareTerms(left, right)), '-1', 'first local comparison');
|
|
2458
|
+
assertEqual(String(compareTerms(right, left)), '-1', 'second local comparison is independent');
|
|
2459
|
+
|
|
2460
|
+
// A sorted-list operation instead supplies one shared ranking context,
|
|
2461
|
+
// so all comparisons made while constructing that list are consistent.
|
|
2462
|
+
const ranks = new Map();
|
|
2463
|
+
assertEqual(String(compareTerms(left, right, ranks)), '-1', 'shared order first direction');
|
|
2464
|
+
assertEqual(String(compareTerms(right, left, ranks)), '1', 'shared order reverse direction');
|
|
2465
|
+
},
|
|
2466
|
+
},
|
|
2446
2467
|
{
|
|
2447
2468
|
name: 'unification rejects direct and indirect cyclic bindings',
|
|
2448
2469
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1811,9 +1811,19 @@ child searches that inherit the solver limit do not stop after a fixed number of
|
|
|
1811
1811
|
solutions. This matters for re-executable goals such as `repeat/0` and for
|
|
1812
1812
|
library relations such as `call_nth/2`; an implementation safety threshold must
|
|
1813
1813
|
not turn a still re-executable search into logical failure. Embedders that need
|
|
1814
|
-
a finite answer budget should pass `solutionLimit` explicitly.
|
|
1815
|
-
|
|
1816
|
-
|
|
1814
|
+
a finite answer budget should pass `solutionLimit` explicitly.
|
|
1815
|
+
|
|
1816
|
+
Variable term order is deliberately scoped rather than stored as a permanent
|
|
1817
|
+
property of a variable. ISO 13211-1 section 7.2.1 leaves the order of two
|
|
1818
|
+
distinct variables implementation dependent and requires constancy only while
|
|
1819
|
+
a sorted list is being created. EyeProlog therefore chooses a local variable
|
|
1820
|
+
ranking for an ordinary term comparison, while `sort/2`, `keysort/2`, and the
|
|
1821
|
+
sorting step of `setof/3` share one ranking for the duration of that single
|
|
1822
|
+
sorted-list operation. No process-global variable registry or creation ordinal
|
|
1823
|
+
is retained or exposed through later comparisons.
|
|
1824
|
+
|
|
1825
|
+
Host capacity failures that V8 reports as `Map maximum size exceeded` or `Set`
|
|
1826
|
+
`maximum size exceeded` are normalized at the solver boundary to the ISO error
|
|
1817
1827
|
`resource_error(finite_memory)` instead of leaking a JavaScript `RangeError`.
|
|
1818
1828
|
|
|
1819
1829
|
### Implementation boundary
|