eyeprolog 1.2.13 → 1.2.15

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/README.md CHANGED
@@ -79,10 +79,12 @@ member_test ?- member(X, [prolog, logic]).
79
79
  ; X = logic.
80
80
  ```
81
81
 
82
- A label may contain several comma-separated metadata fields. When a query has
83
- multiple indented answer descriptions, each description is checked and counted
84
- independently, so one failed expectation does not prevent the later ones from
85
- running.
82
+ A label is simply the first argument of the ordinary `(?-)/2` term, so it may
83
+ use any normal Prolog term syntax; EyeProlog only requires it to be ground when
84
+ the quad is checked. A non-ground label is a quad failure, not a source syntax
85
+ error, and later quads still run. When a query has multiple indented answer
86
+ descriptions, each description is checked and counted independently, so one
87
+ failed expectation does not prevent the later ones from running.
86
88
 
87
89
  ## Strict ISO/IEC 13211-1 core
88
90
 
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.15",
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
@@ -794,13 +794,47 @@ class Parser {
794
794
  accept(clause);
795
795
  continue;
796
796
  }
797
+ // Program clauses historically parse comma separately from the head, so
798
+ // keep that grammar unchanged. A quad id, however, is simply the first
799
+ // argument of the ordinary ?-/2 term. If the initial head parse stops at
800
+ // a comma, tentatively reparse that left operand with the normal term
801
+ // grammar (where comma is the predefined priority-1000 xfy operator).
802
+ // Only when the resulting term is actually followed by ?- do we keep the
803
+ // tentative parse; otherwise restore the parser and retain the existing
804
+ // clause/DCG handling below.
805
+ const headState = {
806
+ pos: this.pos,
807
+ line: this.line,
808
+ anonymous: this.anonymous,
809
+ variables: new Map(this.variables),
810
+ previousToken: this.previousToken,
811
+ token: this.token,
812
+ };
813
+ const restoreHeadState = () => {
814
+ this.pos = headState.pos;
815
+ this.line = headState.line;
816
+ this.anonymous = headState.anonymous;
817
+ this.variables = new Map(headState.variables);
818
+ this.previousToken = headState.previousToken;
819
+ this.token = headState.token;
820
+ };
821
+
797
822
  let head = this.parseTerm(3);
798
- // Both a quad label and a TS 13211-3 semicontext may contain one or more
799
- // unparenthesized commas before their priority-1200 operator. Parse the
800
- // complete comma sequence here instead of stopping after one separator;
801
- // portable quad labels commonly carry several metadata fields, e.g.
802
- // `9, "case", passes ?- Goal.`. Build the standard right-associative
803
- // comma term so this is the same label as `(9, "case", passes)`.
823
+ if (this.token.type === TOK.COMMA && !this.strictIso) {
824
+ restoreHeadState();
825
+ const quadId = this.parseTerm(3, true);
826
+ if (this.operatorTokenName() === '?-') {
827
+ this.advance();
828
+ this.parseQuad(quadId, line, accept);
829
+ continue;
830
+ }
831
+ restoreHeadState();
832
+ head = this.parseTerm(3);
833
+ }
834
+
835
+ // Outside quad syntax, preserve the existing program-level comma rule,
836
+ // including the TS 13211-3 semicontext boundary. This is deliberately
837
+ // not a grammar for quad ids.
804
838
  if (this.token.type === TOK.COMMA) {
805
839
  const items = [head];
806
840
  let extraCommaLine = null;
@@ -809,9 +843,6 @@ class Parser {
809
843
  this.advance();
810
844
  items.push(this.parseTerm(3));
811
845
  }
812
- // Historically the program grammar admitted exactly one comma in a
813
- // DCG semicontext. Keep that boundary: the broader comma sequence is
814
- // specifically the quad-label extension, not a new DCG syntax.
815
846
  if (extraCommaLine != null && this.operatorTokenName() !== '?-') {
816
847
  throw new Error(`parse line ${extraCommaLine}: expected ., got ,`);
817
848
  }
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';
@@ -278,8 +279,11 @@ why(
278
279
  },
279
280
  },
280
281
  {
281
- name: 'quad labels accept multiple metadata fields and each answer description is independent',
282
+ name: 'quad ids use ordinary term syntax and each answer description is independent',
282
283
  run: () => {
284
+ // Issue #21: the first argument of ?-/2 is an ordinary Prolog term.
285
+ // The commas here are the normal priority-1000 comma operator, not a
286
+ // special metadata grammar owned by the quad parser.
283
287
  const source = `9, "✳54·43", passes
284
288
  ` +
285
289
  `?- X is 1+1.
@@ -297,14 +301,50 @@ why(
297
301
  const program = Program.parseSources([{ text: source, filename: 'issue-21.pl' }]);
298
302
  assertEqual(program.quads.length, 1, 'query group count');
299
303
  assertEqual(program.quads[0].answers.length, 4, 'answer-description count');
300
- assertEqual(program.quads[0].id.name, ',', 'outer label comma');
301
- assertEqual(program.quads[0].id.args[1].name, ',', 'right-associated label comma');
304
+ assertEqual(program.quads[0].id.name, ',', 'ordinary outer comma operator');
305
+ assertEqual(program.quads[0].id.args[1].name, ',', 'ordinary right-associated comma operator');
302
306
  const result = publicApi.runQuads(program);
303
307
  assertEqual(result.total, 4, 'answer-description total');
304
308
  assertEqual(result.passed, 4, 'answer-description passed');
305
309
  assertEqual(result.failed, 0, 'answer-description failed');
306
310
  assertEqual(result.stdout, 'quads: 4 run, 4 passed, 0 failed.\n', 'issue #21 report');
307
311
 
312
+ // No convention is imposed on the id term. Functional/list/curly and
313
+ // non-comma operator forms all go through the same ordinary term parser.
314
+ const ordinaryIds = Program.parse(
315
+ `meta(9, passes) ?- true.
316
+ true.
317
+ ` +
318
+ `[9, passes] ?- true.
319
+ true.
320
+ ` +
321
+ `{passes} ?- true.
322
+ true.
323
+ ` +
324
+ `(alpha ; beta) ?- true.
325
+ true.
326
+ `,
327
+ );
328
+ assertEqual(ordinaryIds.quads.length, 4, 'ordinary id term count');
329
+ assertEqual(publicApi.runQuads(ordinaryIds).stdout, 'quads: 4 run, 4 passed, 0 failed.\n',
330
+ 'ordinary id term report');
331
+
332
+ // Groundness is a quad semantic check, not source syntax. A bad id is
333
+ // reported as a test failure and processing continues to the next quad.
334
+ const nonGround = publicApi.runQuads(
335
+ `Id ?- true.
336
+ true.
337
+ ` +
338
+ `ok ?- true.
339
+ true.
340
+ `,
341
+ );
342
+ assertEqual(nonGround.total, 2, 'non-ground id does not abort parsing');
343
+ assertEqual(nonGround.passed, 1, 'following quad still passes');
344
+ assertEqual(nonGround.failed, 1, 'non-ground id is a quad failure');
345
+ assertIncludes(nonGround.stdout, 'quads: BAD_ID Id, <input>:1', 'non-ground id diagnostic');
346
+ assertIncludes(nonGround.stdout, 'quads: 2 run, 1 passed, 1 failed.', 'non-ground continuation summary');
347
+
308
348
  const continuing = publicApi.runQuads(
309
349
  `case ?- X is 1+1.
310
350
  X = 3.
@@ -2443,6 +2483,26 @@ function whiteBoxCases() {
2443
2483
  assertEqual(termToString(variable('X'), env, true), 'socrates', 'binding');
2444
2484
  },
2445
2485
  },
2486
+ {
2487
+ name: 'variable term order is scoped to one comparison or sorted-list operation',
2488
+ run: () => {
2489
+ const left = variable('Left');
2490
+ const right = variable('Right');
2491
+ assertEqual(String(left.order), 'undefined', 'variables carry no persistent order ordinal');
2492
+ assertEqual(String(right.order), 'undefined', 'second variable carries no persistent order ordinal');
2493
+
2494
+ // Separate comparisons are permitted to choose their own
2495
+ // implementation-dependent order under ISO 7.2.1.
2496
+ assertEqual(String(compareTerms(left, right)), '-1', 'first local comparison');
2497
+ assertEqual(String(compareTerms(right, left)), '-1', 'second local comparison is independent');
2498
+
2499
+ // A sorted-list operation instead supplies one shared ranking context,
2500
+ // so all comparisons made while constructing that list are consistent.
2501
+ const ranks = new Map();
2502
+ assertEqual(String(compareTerms(left, right, ranks)), '-1', 'shared order first direction');
2503
+ assertEqual(String(compareTerms(right, left, ranks)), '1', 'shared order reverse direction');
2504
+ },
2505
+ },
2446
2506
  {
2447
2507
  name: 'unification rejects direct and indirect cyclic bindings',
2448
2508
  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
@@ -5160,8 +5170,11 @@ from the parsed `?-/1` or `?-/2` term rather than from one privileged surface
5160
5170
  spelling. Thus `Label ?- Query.`, `?-(Label, Query).`, mixed forms such as
5161
5171
  `?-((Label), Query).`, quoted-functor notation, and a parenthesized whole
5162
5172
  `(?-(Label, Query)).` denote the same quad when followed by indented answer
5163
- descriptions. In `--iso-strict` mode this quad interpretation is disabled, and
5164
- `?-/2` remains ordinary Prolog term syntax.
5173
+ descriptions. `Label` itself is parsed with the ordinary Prolog term grammar:
5174
+ there is no quad-specific comma or metadata syntax. The runner requires the
5175
+ resulting first argument to be ground; if it is not, that quad is reported as
5176
+ `BAD_ID` and later quads are still processed. In `--iso-strict` mode this quad
5177
+ interpretation is disabled, and `?-/2` remains ordinary Prolog term syntax.
5165
5178
 
5166
5179
  Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
5167
5180
  for an explicitly stateful queue and
@@ -6391,7 +6404,11 @@ colors ?- color(X).
6391
6404
  ```
6392
6405
 
6393
6406
  Run all quads in a file with `eyeprolog --quads file.pl` or `eyeprolog -q
6394
- file.pl`. A label such as `colors` is optional. Loading the file normally only
6407
+ file.pl`. A label such as `colors` is optional. A label is not a separate
6408
+ mini-language: it is the ordinary first argument of `(?-)/2`, and therefore may
6409
+ be any Prolog term admitted there by the normal term grammar. Quad execution
6410
+ requires that argument to be ground; a non-ground label is reported as a quad
6411
+ failure rather than aborting source parsing. Loading the file normally only
6395
6412
  records its quads; it does not execute them or add their queries and answers as
6396
6413
  program clauses. A quad run prints a summary and exits with status `1` when any
6397
6414
  description fails. Quad mode imports `library(prologue)` as a compatibility