eyeprolog 1.2.10 → 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
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
11
11
  | builtins | 11 | 0 | 0 | 0 | 11 |
12
12
  | context | 11 | 0 | 0 | 0 | 11 |
13
13
  | control | 15 | 0 | 0 | 0 | 15 |
14
- | iso | 167 | 216 | 0 | 0 | 383 |
14
+ | iso | 167 | 218 | 0 | 0 | 385 |
15
15
  | lists | 52 | 3 | 0 | 0 | 55 |
16
16
  | modules | 2 | 0 | 0 | 0 | 2 |
17
17
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
23
23
  | terms | 26 | 3 | 0 | 0 | 29 |
24
24
  | unification | 18 | 0 | 0 | 0 | 18 |
25
25
  | variables | 16 | 9 | 0 | 0 | 25 |
26
- | **Total** | **482** | **267** | **19** | **21** | **789** |
26
+ | **Total** | **482** | **269** | **19** | **21** | **791** |
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.10",
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/iso.js CHANGED
@@ -1564,6 +1564,13 @@ function parseIsoNumber(text) {
1564
1564
  }
1565
1565
 
1566
1566
  const numericText = `${sign}${text.slice(position)}`;
1567
+ // 8.16.7/8.16.8 parse the character sequence according to the syntax rules
1568
+ // for numbers and negative numbers (6.3.1.1/6.3.1.2), not as an arbitrary
1569
+ // term whose value happens to be numeric. Every such number starts with a
1570
+ // decimal digit after an optional negative sign, so parenthesized terms such
1571
+ // as `(0)` or `-(0)` must be rejected before the general term parser sees
1572
+ // them. This keeps the parser reuse below from admitting grouping syntax.
1573
+ if (!/^-?\d/.test(numericText)) return null;
1567
1574
  // ISO floating-point syntax requires a decimal fraction before an exponent.
1568
1575
  if (/^-?\d+[eE][+-]?\d+$/.test(numericText)) return null;
1569
1576
  try {
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`;
@@ -16,7 +16,7 @@ compliance audit and the remaining work before a full conformance claim.
16
16
  | 8.8-8.10 database and solutions | logical update view, dynamic mutation, all-solutions grouping | `dynamic_database`, `trealla_logical_update_view`, `corrigenda_retractall`, `grouped_solutions_and_clauses` |
17
17
  | 8.11-8.14 streams and term I/O | text/binary streams, properties, units, read/write options and operators | `streams_and_term_io`, `operators`, Corrigendum 3 option cases, stream error cases |
18
18
  | 8.15 logic and control | negation, once, repeat, `call/2` through `call/8`, `false/0` | `logtalk_once`, `corrigenda_call_closure`, `false_builtin` |
19
- | 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, Logtalk-derived error cases |
19
+ | 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, parenthesized-number rejection, Logtalk-derived error cases |
20
20
  | 8.17 flags and hooks | required flags, mutation permissions, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases |
21
21
  | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power` |
22
22
  | ISO/IEC 13211-2 modules | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
@@ -102,7 +102,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
102
102
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
103
103
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
104
104
 
105
- The corpus has 383 cases in `iso/` and 789 file-based conformance cases in
105
+ The corpus has 385 cases in `iso/` and 791 file-based conformance cases in
106
106
  total. The generated `conformance-report.md` is the authoritative source for
107
107
  current category totals. Together with regression, documentation-sync, API,
108
108
  example, and book-example checks, `npm test` is the release gate.
@@ -0,0 +1,2 @@
1
+ %% goal: number_chars(_, ['(', '0', ')'])
2
+ answer :- number_chars(_, ['(', '0', ')']).
@@ -0,0 +1,2 @@
1
+ %% goal: number_codes(_, [40, 48, 41])
2
+ answer :- number_codes(_, [40, 48, 41]).
@@ -0,0 +1 @@
1
+ error(syntax_error(number))
@@ -0,0 +1 @@
1
+ error(syntax_error(number))
@@ -36,6 +36,9 @@
36
36
  70 ?- number_chars(N,"0x11").
37
37
  N=17.
38
38
 
39
+ 73 ?- number_chars(N,"(0)").
40
+ syntax_error(...).
41
+
39
42
  4 ?- number_chars(1,"a").
40
43
  syntax_error(...).
41
44
 
@@ -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,24 @@ c4 ?- call((!;1)).
396
436
  text: source,
397
437
  filename,
398
438
  }]));
399
- assertEqual(result.total, 72, 'quad total');
400
- assertEqual(result.passed, 72, 'quad passed');
401
- assertEqual(result.stdout, 'quads: 72 run, 72 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');
442
+ },
443
+ },
444
+ {
445
+ name: 'number conversion rejects parenthesized numeric terms',
446
+ run: () => {
447
+ for (const goal of ['number_chars(N,"(0)")', 'number_codes(N,[40,48,41])']) {
448
+ let caught = null;
449
+ try {
450
+ publicApi.run('', { goal });
451
+ } catch (error) {
452
+ caught = error;
453
+ }
454
+ if (caught == null) throw new Error(`${goal} should throw`);
455
+ assertIncludes(String(caught?.message ?? caught), 'syntax_error(number)', goal);
456
+ }
402
457
  },
403
458
  },
404
459
  {
@@ -5735,13 +5735,16 @@ quoted_atom("ab"). % quoted_atom(ab)
5735
5735
  | `sub_atom(+Atom,?Before,?Length,?After,?SubAtom)` | Enumerates substrings and their Unicode-code-point offsets. Supplied counts must be nonnegative integers. |
5736
5736
  | `atom_chars(?Atom,?Chars)`, `atom_codes(?Atom,?Codes)` | Convert between an atom and a proper list of one-character atoms or Unicode scalar codes. At least one side must be instantiated. |
5737
5737
  | `char_code(?Character,?Code)` | Converts one character atom and one Unicode scalar code. Surrogates and values outside `0..0x10ffff` raise a representation error. |
5738
- | `number_chars(?Number,?Chars)`, `number_codes(?Number,?Codes)` | Convert finite numbers to canonical text or parse a proper character/code list using ISO numeric-token syntax, including radix integers, character-code constants, and leading layout. At least one side must be instantiated; malformed numeric input raises *syntax_error(number)*. |
5738
+ | `number_chars(?Number,?Chars)`, `number_codes(?Number,?Codes)` | Convert finite numbers to canonical text or parse a proper character/code list using ISO number and negative-number syntax, including radix integers, character-code constants, and leading layout. The input is not parsed as a general term: grouping such as `(0)` is a syntax error. At least one side must be instantiated; malformed numeric input raises *syntax_error(number)*. |
5739
5739
 
5740
5740
  Conversions accept partial output lists when the atomic input is known, but
5741
5741
  constructing an atom or number requires a complete proper list with no unbound
5742
5742
  elements. Numeric parsing accepts leading ISO layout characters, an optional
5743
5743
  sign, decimal fractions, and decimal exponents; it rejects trailing material
5744
- and non-finite values.
5744
+ and non-finite values. The regression gate vendors all 73 numbered cases from
5745
+ Ulrich Neumerkel's contemporary `number_chars/2` comparison, including the
5746
+ Cor.2 error-precedence cases; `number_codes/2` shares the same numeric parser
5747
+ and has mirrored coverage for the parenthesized-number regression.
5745
5748
 
5746
5749
  ### Streams and unit I/O
5747
5750
 
@@ -6405,8 +6408,10 @@ descriptions; variables introduced only by a description are fresh. For example,
6405
6408
  a query `throw(g(X))` is described by `throw(g(_X))`, while
6406
6409
  `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
6407
6410
  variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. Multiple
6408
- indented descriptions after one query must all hold. `inputs/1` supplies and
6409
- 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
6410
6415
  an answer description that this finite-tree implementation skips. `loops` is
6411
6416
  checked with a deterministic solver-depth budget. The advanced stream
6412
6417
  annotations `peeks/1` and `waits`, and the unordered `other_answer_sequence`
@@ -6425,10 +6430,12 @@ console.log(report.passed, report.failed, report.stdout);
6425
6430
  The syntax follows the “queries using answer descriptions” convention used by
6426
6431
  Trealla and the ISO Prolog working examples. Because answer descriptions are
6427
6432
  layout-sensitive, indent every description while keeping ordinary clause heads
6428
- and the next quad query at the left margin. A comma may be part of a quad label,
6429
- including across layout before `?-`. Canonical functional notation is
6430
- semantically equivalent: `?-(Label, Query).` followed by the same indented
6431
- 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.`.
6432
6439
 
6433
6440
  Statistics are comparative evidence, not a score in isolation. Preserve the
6434
6441
  program, input, runtime version, selected query, answers, and counters together.
@@ -6986,7 +6993,7 @@ precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
6986
6993
  maps language families to representative executable cases.
6987
6994
 
6988
6995
  The complete suite must pass before release. The file-based conformance corpus
6989
- contains 789 cases, including 383 focused ISO
6996
+ contains 791 cases, including 385 focused ISO
6990
6997
  cases derived from the success, failure, mode, and error behavior in
6991
6998
  ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
6992
6999
  Separate exact-output suites check 189 normal