eyeprolog 1.2.12 → 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.12",
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/parser.js CHANGED
@@ -746,11 +746,11 @@ class Parser {
746
746
  const accept = emit ?? ((clause) => clauses.push(clause));
747
747
  while (this.token.type !== TOK.EOF) {
748
748
  const line = this.token.line;
749
- // In the normal EyeProlog profile, canonical functional ?-/1 and
750
- // ?-/2 notation denotes the same quad marker as the corresponding
751
- // operator notation. Keep the existing operator-form query parsing so
752
- // a query containing comma remains wholly to the right of `?-`, while
753
- // functional notation is parsed as a term and then decomposed by arity.
749
+ // Prefix operator notation needs one program-level distinction so
750
+ // a comma in `?- A, B.` remains inside the query rather than outside the
751
+ // prefix term. Ordinary functional notation is parsed as a term and then
752
+ // recognized structurally by parseQuadTerm; further equivalent spellings
753
+ // are recognized after the general head parser below.
754
754
  if (this.operatorTokenName() === '?-' && !this.strictIso) {
755
755
  if (this.peek() === '(') {
756
756
  const quadTerm = this.parseTerm(0, true);
@@ -818,6 +818,14 @@ class Parser {
818
818
  head = items.pop();
819
819
  while (items.length > 0) head = compound(',', [items.pop(), head]);
820
820
  }
821
+ // Parentheses and other ordinary term syntax may hide the surface ?-
822
+ // token from the program-level dispatch above. Once the complete head
823
+ // term has been parsed, recognize the same ?-/1 or ?-/2 structure here.
824
+ // Requiring the following dot prevents an ordinary rule whose head just
825
+ // happens to be ?-/1 or ?-/2 from being consumed as a quad mid-clause.
826
+ if (!this.strictIso && this.token.type === TOK.DOT && this.parseQuadTerm(head, line, accept)) {
827
+ continue;
828
+ }
821
829
  if (this.operatorTokenName() === '?-') {
822
830
  if (this.strictIso) {
823
831
  // There is no predefined infix ?-/2 in strict core mode. If a
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';
@@ -222,7 +223,7 @@ why(
222
223
  },
223
224
  },
224
225
  {
225
- name: 'quad parser treats operator and functional ?- notation equivalently',
226
+ name: 'quad parser treats regular term spellings of ?- equivalently',
226
227
  run: () => {
227
228
  const labelled = Program.parse(
228
229
  `0,passes
@@ -253,9 +254,28 @@ why(
253
254
  const functionalReport = publicApi.runQuads(functional);
254
255
  assertEqual(functionalReport.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'functional quad report');
255
256
 
256
- const strict = Program.parse(`?-(','(0,passes),=(X,1)).\n`, { isoStrict: true });
257
+ // Issue #11 is about ordinary term syntax, not one privileged
258
+ // canonical spelling. Parentheses, quoted functor syntax, and mixed
259
+ // operator/functional notation must all denote the same ?-/2 term and
260
+ // therefore the same quad in the normal EyeProlog profile.
261
+ for (const [name, source] of [
262
+ ['mixed', `?-((0,passes), X = 1).\n X = 1.\n`],
263
+ ['parenthesized', `(?-(','(0,passes),=(X,1))).\n X = 1.\n`],
264
+ ['parenthesized mixed', `(?-((0,passes), X = 1)).\n X = 1.\n`],
265
+ ['quoted functor', `'?-'(','(0,passes),=(X,1)).\n X = 1.\n`],
266
+ ['quoted parenthesized', `('?-'(','(0,passes),=(X,1))).\n X = 1.\n`],
267
+ ]) {
268
+ const regular = Program.parse(source);
269
+ assertEqual(regular.quads.length, 1, `${name} quad count`);
270
+ assertEqual(regular.clauses.length, 0, `${name} quad clause count`);
271
+ assertEqual(termToString(regular.quads[0].id), termToString(labelled.quads[0].id), `${name} label`);
272
+ assertEqual(termToString(regular.quads[0].query), termToString(labelled.quads[0].query), `${name} query`);
273
+ assertEqual(publicApi.runQuads(regular).stdout, 'quads: 1 run, 1 passed, 0 failed.\n', `${name} report`);
274
+ }
275
+
276
+ const strict = Program.parse(`(?-(','(0,passes),=(X,1))).\n`, { isoStrict: true });
257
277
  assertEqual(strict.quads.length, 0, 'strict mode has no quads');
258
- assertEqual(strict.clauses.length, 1, 'strict functional ?-/2 remains an ordinary term');
278
+ assertEqual(strict.clauses.length, 1, 'strict ?-/2 remains an ordinary term');
259
279
  },
260
280
  },
261
281
  {
@@ -2424,6 +2444,26 @@ function whiteBoxCases() {
2424
2444
  assertEqual(termToString(variable('X'), env, true), 'socrates', 'binding');
2425
2445
  },
2426
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
+ },
2427
2467
  {
2428
2468
  name: 'unification rejects direct and indirect cyclic bindings',
2429
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
@@ -5156,10 +5166,12 @@ an optional label before the query marker (`Label ?- Query.`), so while quad
5156
5166
  syntax is supported it additionally exposes `?-` at priority 1200 with
5157
5167
  specifier `xfx` as an implementation-specific operator. Consequently
5158
5168
  `current_op(Priority, Specifier, ?-)` enumerates both definitions. At top level in the normal EyeProlog profile, the quad marker is recognized
5159
- after term parsing, so equivalent syntax stays equivalent: `Label ?- Query.`
5160
- and canonical `?-(Label, Query).` denote the same labelled quad when followed
5161
- by its indented answer descriptions. In `--iso-strict` mode this quad
5162
- interpretation is disabled, and `?-/2` remains ordinary Prolog term syntax.
5169
+ from the parsed `?-/1` or `?-/2` term rather than from one privileged surface
5170
+ spelling. Thus `Label ?- Query.`, `?-(Label, Query).`, mixed forms such as
5171
+ `?-((Label), Query).`, quoted-functor notation, and a parenthesized whole
5172
+ `(?-(Label, Query)).` denote the same quad when followed by indented answer
5173
+ descriptions. In `--iso-strict` mode this quad interpretation is disabled, and
5174
+ `?-/2` remains ordinary Prolog term syntax.
5163
5175
 
5164
5176
  Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
5165
5177
  for an explicitly stateful queue and
@@ -6432,9 +6444,11 @@ Trealla and the ISO Prolog working examples. Because answer descriptions are
6432
6444
  layout-sensitive, indent every description while keeping ordinary clause heads
6433
6445
  and the next quad query at the left margin. A quad label may contain any number
6434
6446
  of comma-separated metadata fields, including across layout before `?-`; for
6435
- example `9, "case", passes ?- Goal.` is one labelled query. Canonical
6436
- functional notation is semantically equivalent: `?-(Label, Query).` followed by
6437
- the same indented answer descriptions creates the same labelled quad as
6447
+ example `9, "case", passes ?- Goal.` is one labelled query. Quad recognition
6448
+ is structural after ordinary term parsing: functional, mixed, quoted-functor,
6449
+ and parenthesized spellings of the same `?-/1` or `?-/2` term are semantically
6450
+ equivalent. For example `?-(Label, Query).` and `(?-(Label, Query)).`, followed
6451
+ by the same indented answer descriptions, create the same labelled quad as
6438
6452
  `Label ?- Query.`.
6439
6453
 
6440
6454
  Statistics are comparative evidence, not a score in isolation. Preserve the