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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.13",
6
+ "version": "1.2.14",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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 sorted = [...items].sort((a, b) => resolvedOrder(a, b, env));
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
- const sorted = [...items].sort((a, b) => resolvedOrder(deref(a, env).args[0], deref(b, env).args[0], env));
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 sorted = [...items].sort(compareTerms);
1705
- return sorted.filter((item, index) => index === 0 || compareTerms(sorted[index - 1], item) !== 0);
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, value.order), key: `var:${id}` };
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, order = null) => {
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, term.order);
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, resolved.order);
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.order ?? 0;
531
- const rightOrder = right.order ?? 0;
532
- if (leftOrder !== rightOrder) return leftOrder < rightOrder ? -1 : 1;
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 = compareTerms(left.args[i], right.args[i]);
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
- shared_set_variables(Set) :-
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])).
@@ -7,4 +7,4 @@ no_solutions.
7
7
  retrieved(bob, true).
8
8
  retrieved(carol, true).
9
9
  shared_clause(pair(ok, ok)).
10
- shared_set_variables("ab").
10
+ shared_set_variables(ok).
@@ -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: () => {
@@ -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. Host capacity
1815
- failures that V8 reports as `Map maximum size exceeded` or `Set maximum size
1816
- exceeded` are normalized at the solver boundary to the ISO error
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