eyeprolog 1.2.11 → 1.2.12

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.12",
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
@@ -795,12 +795,28 @@ 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]);
804
820
  }
805
821
  if (this.operatorTokenName() === '?-') {
806
822
  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`;
@@ -258,6 +258,46 @@ why(
258
258
  assertEqual(strict.clauses.length, 1, 'strict functional ?-/2 remains an ordinary term');
259
259
  },
260
260
  },
261
+ {
262
+ name: 'quad labels accept multiple metadata fields and each answer description is independent',
263
+ run: () => {
264
+ const source = `9, "✳54·43", passes
265
+ ` +
266
+ `?- X is 1+1.
267
+ ` +
268
+ ` X = 3, unexpected. % almost
269
+ ` +
270
+ ` X = 1, unexpected. % too low
271
+ ` +
272
+ ` X = 2.0, unexpected.
273
+ ` +
274
+ `% and after checking PM:
275
+ ` +
276
+ ` X = 2.
277
+ `;
278
+ const program = Program.parseSources([{ text: source, filename: 'issue-21.pl' }]);
279
+ assertEqual(program.quads.length, 1, 'query group count');
280
+ assertEqual(program.quads[0].answers.length, 4, 'answer-description count');
281
+ assertEqual(program.quads[0].id.name, ',', 'outer label comma');
282
+ assertEqual(program.quads[0].id.args[1].name, ',', 'right-associated label comma');
283
+ const result = publicApi.runQuads(program);
284
+ assertEqual(result.total, 4, 'answer-description total');
285
+ assertEqual(result.passed, 4, 'answer-description passed');
286
+ assertEqual(result.failed, 0, 'answer-description failed');
287
+ assertEqual(result.stdout, 'quads: 4 run, 4 passed, 0 failed.\n', 'issue #21 report');
288
+
289
+ const continuing = publicApi.runQuads(
290
+ `case ?- X is 1+1.
291
+ X = 3.
292
+ X = 2.
293
+ `,
294
+ );
295
+ assertEqual(continuing.total, 2, 'later descriptions still run after failure');
296
+ assertEqual(continuing.passed, 1, 'later passing description counted');
297
+ assertEqual(continuing.failed, 1, 'failed description counted');
298
+ assertIncludes(continuing.stdout, 'quads: 2 run, 1 passed, 1 failed.', 'continuation summary');
299
+ },
300
+ },
261
301
  {
262
302
  name: 'runQuads checks portable answer descriptions',
263
303
  run: () => {
@@ -275,10 +315,10 @@ why(
275
315
  `?- X = 1.\n X = 2, unexpected.\n X = 1.\n\n` +
276
316
  `?- catch(throw(ball), E, true).\n E = ball | error(system_error, ...).\n`;
277
317
  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');
318
+ assertEqual(result.total, 13, 'answer-description total');
319
+ assertEqual(result.passed, 13, 'answer-description passed');
320
+ assertEqual(result.failed, 0, 'answer-description failed');
321
+ assertEqual(result.stdout, 'quads: 13 run, 13 passed, 0 failed.\n', 'quad report');
282
322
  },
283
323
  },
284
324
  {
@@ -288,10 +328,10 @@ why(
288
328
  ` throw(g(_X)).\n` +
289
329
  ` throw(g(X)), unexpected.\n`;
290
330
  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');
331
+ assertEqual(result.total, 2, 'answer-description total');
332
+ assertEqual(result.passed, 2, 'answer-description passed');
333
+ assertEqual(result.failed, 0, 'answer-description failed');
334
+ assertEqual(result.stdout, 'quads: 2 run, 2 passed, 0 failed.\n', 'quad report');
295
335
 
296
336
  const forbiddenFresh = publicApi.runQuads(
297
337
  `?- throw(g(X)).\n throw(g(_X)), unexpected.\n`,
@@ -396,9 +436,9 @@ c4 ?- call((!;1)).
396
436
  text: source,
397
437
  filename,
398
438
  }]));
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');
439
+ assertEqual(result.total, 77, 'answer-description total');
440
+ assertEqual(result.passed, 77, 'answer-description passed');
441
+ assertEqual(result.stdout, 'quads: 77 run, 77 passed, 0 failed.\n', 'quad report');
402
442
  },
403
443
  },
404
444
  {
@@ -6408,8 +6408,10 @@ descriptions; variables introduced only by a description are fresh. For example,
6408
6408
  a query `throw(g(X))` is described by `throw(g(_X))`, while
6409
6409
  `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
6410
6410
  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
6411
+ indented descriptions after one query are independent checks: each re-runs the
6412
+ query, each is counted in the `quads:` summary, and a failing description does
6413
+ not suppress later descriptions for that query. `inputs/1` supplies and checks
6414
+ consumed characters; `outputs/1` checks emitted characters. `sto` marks
6413
6415
  an answer description that this finite-tree implementation skips. `loops` is
6414
6416
  checked with a deterministic solver-depth budget. The advanced stream
6415
6417
  annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
@@ -6428,10 +6430,12 @@ console.log(report.passed, report.failed, report.stdout);
6428
6430
  The syntax follows the “queries using answer descriptions” convention used by
6429
6431
  Trealla and the ISO Prolog working examples. Because answer descriptions are
6430
6432
  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.`.
6433
+ and the next quad query at the left margin. A quad label may contain any number
6434
+ 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
6438
+ `Label ?- Query.`.
6435
6439
 
6436
6440
  Statistics are comparative evidence, not a score in isolation. Preserve the
6437
6441
  program, input, runtime version, selected query, answers, and counters together.