eyeprolog 1.2.11 → 1.2.13

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,6 +79,11 @@ 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.
86
+
82
87
  ## Strict ISO/IEC 13211-1 core
83
88
 
84
89
  For portability and conformance work, run the Part 1 core with Technical
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.11",
6
+ "version": "1.2.13",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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);
@@ -795,12 +795,36 @@ class Parser {
795
795
  continue;
796
796
  }
797
797
  let head = this.parseTerm(3);
798
- // Both a quad label and a TS 13211-3 semicontext may contain an
799
- // unparenthesized comma before their priority-1200 operator. Assemble
800
- // that left operand before deciding whether the marker is ?- or -->.
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)`.
801
804
  if (this.token.type === TOK.COMMA) {
802
- this.advance();
803
- head = compound(',', [head, this.parseTerm(3)]);
805
+ const items = [head];
806
+ let extraCommaLine = null;
807
+ while (this.token.type === TOK.COMMA) {
808
+ if (items.length >= 2 && extraCommaLine == null) extraCommaLine = this.token.line;
809
+ this.advance();
810
+ items.push(this.parseTerm(3));
811
+ }
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
+ if (extraCommaLine != null && this.operatorTokenName() !== '?-') {
816
+ throw new Error(`parse line ${extraCommaLine}: expected ., got ,`);
817
+ }
818
+ head = items.pop();
819
+ while (items.length > 0) head = compound(',', [items.pop(), head]);
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;
804
828
  }
805
829
  if (this.operatorTokenName() === '?-') {
806
830
  if (this.strictIso) {
package/src/quads.js CHANGED
@@ -33,9 +33,14 @@ export function runQuads(source, options = {}) {
33
33
  const results = [];
34
34
  const lines = [];
35
35
  for (const quad of quads) {
36
- const result = checkQuad(program, quad, options);
37
- results.push(result);
38
- if (!result.ok) lines.push(formatFailure(program, quad, result));
36
+ // Every indented answer description is an independent portable quad test.
37
+ // Re-run the query for each description so a failed expectation does not
38
+ // prevent later expectations for the same query from being checked.
39
+ for (const description of quad.answers) {
40
+ const result = checkQuadDescription(program, quad, description, options);
41
+ results.push(result);
42
+ if (!result.ok) lines.push(formatFailure(program, quad, result, description));
43
+ }
39
44
  }
40
45
  const passed = results.filter((result) => result.ok).length;
41
46
  const failed = results.length - passed;
@@ -43,15 +48,11 @@ export function runQuads(source, options = {}) {
43
48
  return { stdout: lines.join(''), total: results.length, passed, failed, results };
44
49
  }
45
50
 
46
- function checkQuad(program, quad, options) {
51
+ function checkQuadDescription(program, quad, description, options) {
47
52
  if (quad.id != null && !termIsGround(quad.id, new Env())) {
48
53
  return { ok: false, kind: 'bad_identifier', expected: quad.id };
49
54
  }
50
- for (const description of quad.answers) {
51
- const checked = checkDescription(program, quad, description, options);
52
- if (!checked.ok) return checked;
53
- }
54
- return { ok: true };
55
+ return checkDescription(program, quad, description, options);
55
56
  }
56
57
 
57
58
  function checkDescription(program, quad, description, options) {
@@ -398,14 +399,14 @@ function splitOperator(term, name) {
398
399
  return [term];
399
400
  }
400
401
 
401
- function formatFailure(program, quad, result) {
402
+ function formatFailure(program, quad, result, description = quad.answers[0]) {
402
403
  const source = quad.source ?? { filename: '<input>', line: 1 };
403
404
  const label = quad.id == null ? '' : `${formatQuadTerm(program, quad.id)}, `;
404
405
  const reason = result.kind === 'malformed' ? 'MALFORMED'
405
406
  : result.kind === 'bad_identifier' ? 'BAD_ID'
406
407
  : result.kind === 'unsupported' ? 'UNSUPPORTED'
407
408
  : 'FAILED';
408
- const expected = result.expected ?? quad.answers[0];
409
+ const expected = result.expected ?? description;
409
410
  return `quads: ${reason} ${label}${source.filename}:${source.line}\n` +
410
411
  ` ?- ${formatQuadTerm(program, quad.query)}.\n` +
411
412
  ` expected: ${formatQuadTerm(program, expected)}.\n`;
@@ -222,7 +222,7 @@ why(
222
222
  },
223
223
  },
224
224
  {
225
- name: 'quad parser treats operator and functional ?- notation equivalently',
225
+ name: 'quad parser treats regular term spellings of ?- equivalently',
226
226
  run: () => {
227
227
  const labelled = Program.parse(
228
228
  `0,passes
@@ -253,9 +253,68 @@ why(
253
253
  const functionalReport = publicApi.runQuads(functional);
254
254
  assertEqual(functionalReport.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'functional quad report');
255
255
 
256
- const strict = Program.parse(`?-(','(0,passes),=(X,1)).\n`, { isoStrict: true });
256
+ // Issue #11 is about ordinary term syntax, not one privileged
257
+ // canonical spelling. Parentheses, quoted functor syntax, and mixed
258
+ // operator/functional notation must all denote the same ?-/2 term and
259
+ // therefore the same quad in the normal EyeProlog profile.
260
+ for (const [name, source] of [
261
+ ['mixed', `?-((0,passes), X = 1).\n X = 1.\n`],
262
+ ['parenthesized', `(?-(','(0,passes),=(X,1))).\n X = 1.\n`],
263
+ ['parenthesized mixed', `(?-((0,passes), X = 1)).\n X = 1.\n`],
264
+ ['quoted functor', `'?-'(','(0,passes),=(X,1)).\n X = 1.\n`],
265
+ ['quoted parenthesized', `('?-'(','(0,passes),=(X,1))).\n X = 1.\n`],
266
+ ]) {
267
+ const regular = Program.parse(source);
268
+ assertEqual(regular.quads.length, 1, `${name} quad count`);
269
+ assertEqual(regular.clauses.length, 0, `${name} quad clause count`);
270
+ assertEqual(termToString(regular.quads[0].id), termToString(labelled.quads[0].id), `${name} label`);
271
+ assertEqual(termToString(regular.quads[0].query), termToString(labelled.quads[0].query), `${name} query`);
272
+ assertEqual(publicApi.runQuads(regular).stdout, 'quads: 1 run, 1 passed, 0 failed.\n', `${name} report`);
273
+ }
274
+
275
+ const strict = Program.parse(`(?-(','(0,passes),=(X,1))).\n`, { isoStrict: true });
257
276
  assertEqual(strict.quads.length, 0, 'strict mode has no quads');
258
- assertEqual(strict.clauses.length, 1, 'strict functional ?-/2 remains an ordinary term');
277
+ assertEqual(strict.clauses.length, 1, 'strict ?-/2 remains an ordinary term');
278
+ },
279
+ },
280
+ {
281
+ name: 'quad labels accept multiple metadata fields and each answer description is independent',
282
+ run: () => {
283
+ const source = `9, "✳54·43", passes
284
+ ` +
285
+ `?- X is 1+1.
286
+ ` +
287
+ ` X = 3, unexpected. % almost
288
+ ` +
289
+ ` X = 1, unexpected. % too low
290
+ ` +
291
+ ` X = 2.0, unexpected.
292
+ ` +
293
+ `% and after checking PM:
294
+ ` +
295
+ ` X = 2.
296
+ `;
297
+ const program = Program.parseSources([{ text: source, filename: 'issue-21.pl' }]);
298
+ assertEqual(program.quads.length, 1, 'query group count');
299
+ 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');
302
+ const result = publicApi.runQuads(program);
303
+ assertEqual(result.total, 4, 'answer-description total');
304
+ assertEqual(result.passed, 4, 'answer-description passed');
305
+ assertEqual(result.failed, 0, 'answer-description failed');
306
+ assertEqual(result.stdout, 'quads: 4 run, 4 passed, 0 failed.\n', 'issue #21 report');
307
+
308
+ const continuing = publicApi.runQuads(
309
+ `case ?- X is 1+1.
310
+ X = 3.
311
+ X = 2.
312
+ `,
313
+ );
314
+ assertEqual(continuing.total, 2, 'later descriptions still run after failure');
315
+ assertEqual(continuing.passed, 1, 'later passing description counted');
316
+ assertEqual(continuing.failed, 1, 'failed description counted');
317
+ assertIncludes(continuing.stdout, 'quads: 2 run, 1 passed, 1 failed.', 'continuation summary');
259
318
  },
260
319
  },
261
320
  {
@@ -275,10 +334,10 @@ why(
275
334
  `?- X = 1.\n X = 2, unexpected.\n X = 1.\n\n` +
276
335
  `?- catch(throw(ball), E, true).\n E = ball | error(system_error, ...).\n`;
277
336
  const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'quads.pl' }]));
278
- assertEqual(result.total, 12, 'quad total');
279
- assertEqual(result.passed, 12, 'quad passed');
280
- assertEqual(result.failed, 0, 'quad failed');
281
- assertEqual(result.stdout, 'quads: 12 run, 12 passed, 0 failed.\n', 'quad report');
337
+ assertEqual(result.total, 13, 'answer-description total');
338
+ assertEqual(result.passed, 13, 'answer-description passed');
339
+ assertEqual(result.failed, 0, 'answer-description failed');
340
+ assertEqual(result.stdout, 'quads: 13 run, 13 passed, 0 failed.\n', 'quad report');
282
341
  },
283
342
  },
284
343
  {
@@ -288,10 +347,10 @@ why(
288
347
  ` throw(g(_X)).\n` +
289
348
  ` throw(g(X)), unexpected.\n`;
290
349
  const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'throw-copy-quad.pl' }]));
291
- assertEqual(result.total, 1, 'quad total');
292
- assertEqual(result.passed, 1, 'quad passed');
293
- assertEqual(result.failed, 0, 'quad failed');
294
- assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
350
+ assertEqual(result.total, 2, 'answer-description total');
351
+ assertEqual(result.passed, 2, 'answer-description passed');
352
+ assertEqual(result.failed, 0, 'answer-description failed');
353
+ assertEqual(result.stdout, 'quads: 2 run, 2 passed, 0 failed.\n', 'quad report');
295
354
 
296
355
  const forbiddenFresh = publicApi.runQuads(
297
356
  `?- throw(g(X)).\n throw(g(_X)), unexpected.\n`,
@@ -396,9 +455,9 @@ c4 ?- call((!;1)).
396
455
  text: source,
397
456
  filename,
398
457
  }]));
399
- assertEqual(result.total, 73, 'quad total');
400
- assertEqual(result.passed, 73, 'quad passed');
401
- assertEqual(result.stdout, 'quads: 73 run, 73 passed, 0 failed.\n', 'quad report');
458
+ assertEqual(result.total, 77, 'answer-description total');
459
+ assertEqual(result.passed, 77, 'answer-description passed');
460
+ assertEqual(result.stdout, 'quads: 77 run, 77 passed, 0 failed.\n', 'quad report');
402
461
  },
403
462
  },
404
463
  {
@@ -5156,10 +5156,12 @@ an optional label before the query marker (`Label ?- Query.`), so while quad
5156
5156
  syntax is supported it additionally exposes `?-` at priority 1200 with
5157
5157
  specifier `xfx` as an implementation-specific operator. Consequently
5158
5158
  `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.
5159
+ from the parsed `?-/1` or `?-/2` term rather than from one privileged surface
5160
+ spelling. Thus `Label ?- Query.`, `?-(Label, Query).`, mixed forms such as
5161
+ `?-((Label), Query).`, quoted-functor notation, and a parenthesized whole
5162
+ `(?-(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.
5163
5165
 
5164
5166
  Run [`iso-dynamic-database.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl)
5165
5167
  for an explicitly stateful queue and
@@ -6408,8 +6410,10 @@ descriptions; variables introduced only by a description are fresh. For example,
6408
6410
  a query `throw(g(X))` is described by `throw(g(_X))`, while
6409
6411
  `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
6410
6412
  variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. Multiple
6411
- indented descriptions after one query must all hold. `inputs/1` supplies and
6412
- checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
6413
+ indented descriptions after one query are independent checks: each re-runs the
6414
+ query, each is counted in the `quads:` summary, and a failing description does
6415
+ not suppress later descriptions for that query. `inputs/1` supplies and checks
6416
+ consumed characters; `outputs/1` checks emitted characters. `sto` marks
6413
6417
  an answer description that this finite-tree implementation skips. `loops` is
6414
6418
  checked with a deterministic solver-depth budget. The advanced stream
6415
6419
  annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
@@ -6428,10 +6432,14 @@ console.log(report.passed, report.failed, report.stdout);
6428
6432
  The syntax follows the “queries using answer descriptions” convention used by
6429
6433
  Trealla and the ISO Prolog working examples. Because answer descriptions are
6430
6434
  layout-sensitive, indent every description while keeping ordinary clause heads
6431
- and the next quad query at the left margin. A comma may be part of a quad label,
6432
- including across layout before `?-`. Canonical functional notation is
6433
- semantically equivalent: `?-(Label, Query).` followed by the same indented
6434
- answer descriptions creates the same labelled quad as `Label ?- Query.`.
6435
+ and the next quad query at the left margin. A quad label may contain any number
6436
+ of comma-separated metadata fields, including across layout before `?-`; for
6437
+ example `9, "case", passes ?- Goal.` is one labelled query. Quad recognition
6438
+ is structural after ordinary term parsing: functional, mixed, quoted-functor,
6439
+ and parenthesized spellings of the same `?-/1` or `?-/2` term are semantically
6440
+ equivalent. For example `?-(Label, Query).` and `(?-(Label, Query)).`, followed
6441
+ by the same indented answer descriptions, create the same labelled quad as
6442
+ `Label ?- Query.`.
6435
6443
 
6436
6444
  Statistics are comparative evidence, not a score in isolation. Preserve the
6437
6445
  program, input, runtime version, selected query, answers, and counters together.