eyeprolog 1.1.0 → 1.1.2

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.
Files changed (159) hide show
  1. package/README.md +12 -2
  2. package/conformance-report.md +2 -2
  3. package/examples/book/README.md +2 -1
  4. package/examples/book/chapter-22/04-sentence.pl +10 -14
  5. package/examples/book/chapter-38/02-look_ahead.pl +2 -0
  6. package/examples/dcg-command-parser.pl +26 -0
  7. package/examples/output/dcg-command-parser.pl +4 -0
  8. package/package.json +1 -1
  9. package/playground.html +1 -0
  10. package/src/dcg.js +245 -0
  11. package/src/iso.js +42 -0
  12. package/src/parser.js +23 -2
  13. package/src/program.js +22 -5
  14. package/src/solver.js +6 -2
  15. package/test/conformance/ISO-MATRIX.md +3 -1
  16. package/test/conformance/README.md +8 -6
  17. package/test/conformance/THIRD_PARTY.md +7 -0
  18. package/test/conformance/cases/iso/cut_disjunction_scope.pl +5 -0
  19. package/test/conformance/cases/iso/dcg_ambiguous_alternatives.pl +8 -0
  20. package/test/conformance/cases/iso/dcg_call_phrase_semicontext.pl +12 -0
  21. package/test/conformance/cases/iso/dcg_control_constructs.pl +12 -0
  22. package/test/conformance/cases/iso/dcg_declaration_nonterminal_indicators.pl +7 -0
  23. package/test/conformance/cases/iso/dcg_double_quotes_atom.pl +7 -0
  24. package/test/conformance/cases/iso/dcg_double_quotes_codes.pl +6 -0
  25. package/test/conformance/cases/iso/dcg_dynamic_body_control.pl +15 -0
  26. package/test/conformance/cases/iso/dcg_dynamic_nonterminal_indicator.pl +7 -0
  27. package/test/conformance/cases/iso/dcg_explicit_qualified_head.pl +6 -0
  28. package/test/conformance/cases/iso/dcg_module_nonterminal_indicator.pl +8 -0
  29. package/test/conformance/cases/iso/dcg_nested_phrase_call.pl +6 -0
  30. package/test/conformance/cases/iso/dcg_operator_table.pl +5 -0
  31. package/test/conformance/cases/iso/dcg_partial_sequences.pl +12 -0
  32. package/test/conformance/cases/iso/dcg_phrase_steadfastness.pl +14 -0
  33. package/test/conformance/cases/iso/dcg_qualified_chain.pl +10 -0
  34. package/test/conformance/cases/iso/dcg_recursive_sequences.pl +8 -0
  35. package/test/conformance/cases/iso/dcg_terminals_and_remainder.pl +8 -0
  36. package/test/conformance/cases/iso/dcg_variable_hygiene.pl +9 -0
  37. package/test/conformance/cases/iso/logtalk_dcg_conjunction_disjunction.pl +14 -0
  38. package/test/conformance/cases/iso/logtalk_dcg_cut.pl +12 -0
  39. package/test/conformance/cases/iso/logtalk_dcg_cut_positions.pl +13 -0
  40. package/test/conformance/cases/iso/logtalk_dcg_embedded_goals.pl +12 -0
  41. package/test/conformance/cases/iso/logtalk_dcg_embedded_sequences.pl +10 -0
  42. package/test/conformance/cases/iso/logtalk_dcg_graphic_nonterminal.pl +7 -0
  43. package/test/conformance/cases/iso/logtalk_dcg_if_then_else.pl +15 -0
  44. package/test/conformance/cases/iso/logtalk_dcg_negation_positions.pl +11 -0
  45. package/test/conformance/cases/iso/logtalk_dcg_nonterminals.pl +11 -0
  46. package/test/conformance/cases/iso/logtalk_dcg_phrase_identity.pl +12 -0
  47. package/test/conformance/cases/iso/logtalk_dcg_semicontext_variants.pl +12 -0
  48. package/test/conformance/cases/iso/logtalk_dcg_semicontexts.pl +12 -0
  49. package/test/conformance/cases/iso/logtalk_dcg_terminal_variables.pl +9 -0
  50. package/test/conformance/cases/iso/logtalk_dcg_terminals.pl +15 -0
  51. package/test/conformance/cases/iso/logtalk_dcg_variable_bodies.pl +10 -0
  52. package/test/conformance/errors/iso/dcg_call_noncallable.pl +3 -0
  53. package/test/conformance/errors/iso/dcg_call_variable.pl +3 -0
  54. package/test/conformance/errors/iso/dcg_nonatom_qualified_module.pl +3 -0
  55. package/test/conformance/errors/iso/dcg_partial_semicontext.pl +3 -0
  56. package/test/conformance/errors/iso/dcg_phrase_bad_input.pl +1 -0
  57. package/test/conformance/errors/iso/dcg_phrase_bad_output.pl +1 -0
  58. package/test/conformance/errors/iso/dcg_phrase_noncallable.pl +1 -0
  59. package/test/conformance/errors/iso/dcg_phrase_nonterminal_variable.pl +3 -0
  60. package/test/conformance/errors/iso/dcg_phrase_variable.pl +1 -0
  61. package/test/conformance/errors/iso/dcg_qualified_unknown_nonterminal.pl +4 -0
  62. package/test/conformance/errors/iso/dcg_variable_qualified_module.pl +3 -0
  63. package/test/conformance/errors/iso/dcg_variable_rule_body.pl +3 -0
  64. package/test/conformance/errors/iso/dcg_variable_rule_head.pl +3 -0
  65. package/test/conformance/errors/iso/logtalk_dcg_bad_input_precedence.pl +2 -0
  66. package/test/conformance/errors/iso/logtalk_dcg_bad_output_precedence.pl +2 -0
  67. package/test/conformance/errors/iso/logtalk_dcg_curly_head.pl +3 -0
  68. package/test/conformance/errors/iso/logtalk_dcg_cut_head.pl +3 -0
  69. package/test/conformance/errors/iso/logtalk_dcg_embedded_noncallable.pl +2 -0
  70. package/test/conformance/errors/iso/logtalk_dcg_improper_semicontext.pl +3 -0
  71. package/test/conformance/errors/iso/logtalk_dcg_improper_terminal_rule.pl +3 -0
  72. package/test/conformance/errors/iso/logtalk_dcg_invalid_semicontext.pl +3 -0
  73. package/test/conformance/errors/iso/logtalk_dcg_multiple_semicontexts.pl +3 -0
  74. package/test/conformance/errors/iso/logtalk_dcg_nested_noncallable.pl +2 -0
  75. package/test/conformance/errors/iso/logtalk_dcg_noncallable_after_failure.pl +2 -0
  76. package/test/conformance/errors/iso/logtalk_dcg_noncallable_after_success.pl +2 -0
  77. package/test/conformance/errors/iso/logtalk_dcg_noncallable_precedence.pl +2 -0
  78. package/test/conformance/errors/iso/logtalk_dcg_numeric_head.pl +3 -0
  79. package/test/conformance/errors/iso/logtalk_dcg_numeric_nonterminal.pl +3 -0
  80. package/test/conformance/errors/iso/logtalk_dcg_partial_terminal_rule.pl +4 -0
  81. package/test/conformance/errors/iso/logtalk_dcg_phrase_curly_variable.pl +2 -0
  82. package/test/conformance/errors/iso/logtalk_dcg_phrase_improper_input.pl +2 -0
  83. package/test/conformance/errors/iso/logtalk_dcg_phrase_improper_output.pl +2 -0
  84. package/test/conformance/errors/iso/logtalk_dcg_reversed_semicontext.pl +3 -0
  85. package/test/conformance/errors/iso/logtalk_dcg_terminal_head.pl +3 -0
  86. package/test/conformance/errors/iso/logtalk_dcg_unknown_nonterminal.pl +2 -0
  87. package/test/conformance/expected/iso/cut_disjunction_scope.pl +2 -0
  88. package/test/conformance/expected/iso/dcg_ambiguous_alternatives.pl +1 -0
  89. package/test/conformance/expected/iso/dcg_call_phrase_semicontext.pl +4 -0
  90. package/test/conformance/expected/iso/dcg_control_constructs.pl +6 -0
  91. package/test/conformance/expected/iso/dcg_declaration_nonterminal_indicators.pl +1 -0
  92. package/test/conformance/expected/iso/dcg_double_quotes_atom.pl +1 -0
  93. package/test/conformance/expected/iso/dcg_double_quotes_codes.pl +1 -0
  94. package/test/conformance/expected/iso/dcg_dynamic_body_control.pl +5 -0
  95. package/test/conformance/expected/iso/dcg_dynamic_nonterminal_indicator.pl +1 -0
  96. package/test/conformance/expected/iso/dcg_explicit_qualified_head.pl +1 -0
  97. package/test/conformance/expected/iso/dcg_module_nonterminal_indicator.pl +2 -0
  98. package/test/conformance/expected/iso/dcg_nested_phrase_call.pl +1 -0
  99. package/test/conformance/expected/iso/dcg_operator_table.pl +2 -0
  100. package/test/conformance/expected/iso/dcg_partial_sequences.pl +3 -0
  101. package/test/conformance/expected/iso/dcg_phrase_steadfastness.pl +4 -0
  102. package/test/conformance/expected/iso/dcg_qualified_chain.pl +2 -0
  103. package/test/conformance/expected/iso/dcg_recursive_sequences.pl +3 -0
  104. package/test/conformance/expected/iso/dcg_terminals_and_remainder.pl +2 -0
  105. package/test/conformance/expected/iso/dcg_variable_hygiene.pl +2 -0
  106. package/test/conformance/expected/iso/logtalk_dcg_conjunction_disjunction.pl +8 -0
  107. package/test/conformance/expected/iso/logtalk_dcg_cut.pl +4 -0
  108. package/test/conformance/expected/iso/logtalk_dcg_cut_positions.pl +4 -0
  109. package/test/conformance/expected/iso/logtalk_dcg_embedded_goals.pl +4 -0
  110. package/test/conformance/expected/iso/logtalk_dcg_embedded_sequences.pl +3 -0
  111. package/test/conformance/expected/iso/logtalk_dcg_graphic_nonterminal.pl +1 -0
  112. package/test/conformance/expected/iso/logtalk_dcg_if_then_else.pl +7 -0
  113. package/test/conformance/expected/iso/logtalk_dcg_negation_positions.pl +4 -0
  114. package/test/conformance/expected/iso/logtalk_dcg_nonterminals.pl +3 -0
  115. package/test/conformance/expected/iso/logtalk_dcg_phrase_identity.pl +6 -0
  116. package/test/conformance/expected/iso/logtalk_dcg_semicontext_variants.pl +4 -0
  117. package/test/conformance/expected/iso/logtalk_dcg_semicontexts.pl +4 -0
  118. package/test/conformance/expected/iso/logtalk_dcg_terminal_variables.pl +3 -0
  119. package/test/conformance/expected/iso/logtalk_dcg_terminals.pl +6 -0
  120. package/test/conformance/expected/iso/logtalk_dcg_variable_bodies.pl +4 -0
  121. package/test/conformance/expected-errors/iso/dcg_call_noncallable.txt +1 -0
  122. package/test/conformance/expected-errors/iso/dcg_call_variable.txt +1 -0
  123. package/test/conformance/expected-errors/iso/dcg_nonatom_qualified_module.txt +1 -0
  124. package/test/conformance/expected-errors/iso/dcg_partial_semicontext.txt +1 -0
  125. package/test/conformance/expected-errors/iso/dcg_phrase_bad_input.txt +1 -0
  126. package/test/conformance/expected-errors/iso/dcg_phrase_bad_output.txt +1 -0
  127. package/test/conformance/expected-errors/iso/dcg_phrase_noncallable.txt +1 -0
  128. package/test/conformance/expected-errors/iso/dcg_phrase_nonterminal_variable.txt +1 -0
  129. package/test/conformance/expected-errors/iso/dcg_phrase_variable.txt +1 -0
  130. package/test/conformance/expected-errors/iso/dcg_qualified_unknown_nonterminal.txt +1 -0
  131. package/test/conformance/expected-errors/iso/dcg_variable_qualified_module.txt +1 -0
  132. package/test/conformance/expected-errors/iso/dcg_variable_rule_body.txt +1 -0
  133. package/test/conformance/expected-errors/iso/dcg_variable_rule_head.txt +1 -0
  134. package/test/conformance/expected-errors/iso/logtalk_dcg_bad_input_precedence.txt +1 -0
  135. package/test/conformance/expected-errors/iso/logtalk_dcg_bad_output_precedence.txt +1 -0
  136. package/test/conformance/expected-errors/iso/logtalk_dcg_curly_head.txt +1 -0
  137. package/test/conformance/expected-errors/iso/logtalk_dcg_cut_head.txt +1 -0
  138. package/test/conformance/expected-errors/iso/logtalk_dcg_embedded_noncallable.txt +1 -0
  139. package/test/conformance/expected-errors/iso/logtalk_dcg_improper_semicontext.txt +1 -0
  140. package/test/conformance/expected-errors/iso/logtalk_dcg_improper_terminal_rule.txt +1 -0
  141. package/test/conformance/expected-errors/iso/logtalk_dcg_invalid_semicontext.txt +1 -0
  142. package/test/conformance/expected-errors/iso/logtalk_dcg_multiple_semicontexts.txt +1 -0
  143. package/test/conformance/expected-errors/iso/logtalk_dcg_nested_noncallable.txt +1 -0
  144. package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_failure.txt +1 -0
  145. package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_success.txt +1 -0
  146. package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_precedence.txt +1 -0
  147. package/test/conformance/expected-errors/iso/logtalk_dcg_numeric_head.txt +1 -0
  148. package/test/conformance/expected-errors/iso/logtalk_dcg_numeric_nonterminal.txt +1 -0
  149. package/test/conformance/expected-errors/iso/logtalk_dcg_partial_terminal_rule.txt +1 -0
  150. package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_curly_variable.txt +1 -0
  151. package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_input.txt +1 -0
  152. package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_output.txt +1 -0
  153. package/test/conformance/expected-errors/iso/logtalk_dcg_reversed_semicontext.txt +1 -0
  154. package/test/conformance/expected-errors/iso/logtalk_dcg_terminal_head.txt +1 -0
  155. package/test/conformance/expected-errors/iso/logtalk_dcg_unknown_nonterminal.txt +1 -0
  156. package/test/run-regression.mjs +24 -2
  157. package/the-art-of-eyeprolog.md +89 -39
  158. package/tools/extract-book-examples.mjs +1 -1
  159. package/why-eyeprolog.md +5 -1
package/README.md CHANGED
@@ -33,9 +33,19 @@ Programs may declare their default queries with `%% goal:` comments.
33
33
  Double-quoted text follows the ISO `double_quotes` flag and defaults to a
34
34
  proper list of one-character atoms (`chars`), matching Trealla and Scryer.
35
35
 
36
- ## Standard library modules
36
+ ## ISO modules and definite clause grammars
37
37
 
38
- EyeProlog adds 44 public library predicates to its 127-entry ISO core registry.
38
+ EyeProlog implements ISO/IEC 13211-2 modules and the grammar rules and
39
+ `phrase/2-3` predicates of ISO/IEC TS 13211-3:2025. For example:
40
+
41
+ ```prolog
42
+ sentence --> [hello], noun.
43
+ noun --> [world] | [prolog].
44
+
45
+ %% goal: phrase(sentence, Words)
46
+ ```
47
+
48
+ EyeProlog also adds 44 public library predicates to its 129-entry ISO registry.
39
49
  **All 44 are ordinary Prolog clauses** in `src/lib/eyeprolog.pl` and
40
50
  `src/lib/lists.pl`. They are ISO/IEC 13211-2 modules, loaded explicitly with
41
51
  `use_module(library(eyeprolog))` or `use_module(library(lists))`, following the
@@ -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 | 130 | 174 | 0 | 0 | 304 |
14
+ | iso | 164 | 209 | 0 | 0 | 373 |
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** | **445** | **225** | **19** | **21** | **710** |
26
+ | **Total** | **479** | **260** | **19** | **21** | **779** |
@@ -127,7 +127,7 @@ npm run generate
127
127
  - [01-tree.pl](chapter-22/01-tree.pl)
128
128
  - [02-tree_member.pl](chapter-22/02-tree_member.pl)
129
129
  - [03-mirror.pl](chapter-22/03-mirror.pl) — Transforming a tree
130
- - [04-sentence.pl](chapter-22/04-sentence.pl) — A grammar without special syntax
130
+ - [04-sentence.pl](chapter-22/04-sentence.pl) — A standard definite clause grammar
131
131
  - [05-evaluate.pl](chapter-22/05-evaluate.pl) — Interpreting an expression
132
132
  - [06-lookup.pl](chapter-22/06-lookup.pl)
133
133
  - [07-simplify.pl](chapter-22/07-simplify.pl) — Rewriting symbolic expressions
@@ -233,6 +233,7 @@ npm run generate
233
233
  ## Chapter 38: Language and ISO profile
234
234
 
235
235
  - [01-city.pl](chapter-38/01-city.pl)
236
+ - [02-look_ahead.pl](chapter-38/02-look_ahead.pl) — ISO Part 3 definite clause grammars
236
237
 
237
238
  ## Chapter 39: Built-in predicates by programming role
238
239
 
@@ -1,19 +1,15 @@
1
- % From The Art of EyeProlog, Chapter 22 — A grammar without special syntax.
2
- sentence(Input, Rest) :-
3
- noun_phrase(Input, AfterNoun),
4
- verb_phrase(AfterNoun, Rest).
1
+ % From The Art of EyeProlog, Chapter 22 — A standard definite clause grammar.
2
+ sentence --> noun_phrase, verb_phrase.
5
3
 
6
- noun_phrase([the | Input], Rest) :- noun(Input, Rest).
7
- noun_phrase([a | Input], Rest) :- noun(Input, Rest).
4
+ noun_phrase --> [the], noun.
5
+ noun_phrase --> [a], noun.
8
6
 
9
- noun([robot | Rest], Rest).
10
- noun([scientist | Rest], Rest).
7
+ noun --> [robot].
8
+ noun --> [scientist].
11
9
 
12
- verb_phrase(Input, Rest) :-
13
- verb(Input, AfterVerb),
14
- noun_phrase(AfterVerb, Rest).
10
+ verb_phrase --> verb, noun_phrase.
15
11
 
16
- verb([helps | Rest], Rest).
17
- verb([observes | Rest], Rest).
12
+ verb --> [helps].
13
+ verb --> [observes].
18
14
 
19
- complete_sentence(Words) :- sentence(Words, []).
15
+ complete_sentence(Words) :- phrase(sentence, Words).
@@ -0,0 +1,2 @@
1
+ % From The Art of EyeProlog, Chapter 38 — ISO Part 3 definite clause grammars.
2
+ look_ahead(X), [X] --> [X].
@@ -0,0 +1,26 @@
1
+ % Parse and generate commands for a tiny home-automation language with a DCG.
2
+ %
3
+ % The grammar separates surface tokens from the structured command term used
4
+ % by an application. phrase/2 requires complete input, while phrase/3 leaves
5
+ % unconsumed tokens available to a surrounding parser.
6
+ %% goal: dcg_example(X0, X1)
7
+
8
+ command(set(light(Room), State)) -->
9
+ [set], room(Room), [light, to], state(State).
10
+
11
+ room(kitchen) --> [kitchen].
12
+ room(hall) --> [hall].
13
+
14
+ state(on) --> [on].
15
+ state(off) --> [off].
16
+
17
+ dcg_example(parsed, Command) :-
18
+ phrase(command(Command), [set, kitchen, light, to, on]).
19
+ dcg_example(generated, Tokens) :-
20
+ phrase(command(set(light(hall), off)), Tokens).
21
+ dcg_example(remainder, Rest) :-
22
+ phrase(command(set(light(kitchen), on)),
23
+ [set, kitchen, light, to, on, then, wait],
24
+ Rest).
25
+ dcg_example(rejected, invalid_command) :-
26
+ \+ phrase(command(_), [set, garage, light, to, blinking]).
@@ -0,0 +1,4 @@
1
+ dcg_example(parsed, set(light(kitchen), on)).
2
+ dcg_example(generated, [set, hall, light, to, off]).
3
+ dcg_example(remainder, [then, wait]).
4
+ dcg_example(rejected, invalid_command).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.0",
6
+ "version": "1.1.2",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/playground.html CHANGED
@@ -475,6 +475,7 @@
475
475
  "control-system",
476
476
  "critical-path-schedule",
477
477
  "cyclic-path",
478
+ "dcg-command-parser",
478
479
  "d3-group",
479
480
  "dairy-energy-balance",
480
481
  "data-negotiation",
package/src/dcg.js ADDED
@@ -0,0 +1,245 @@
1
+ // ISO/IEC TS 13211-3:2025 definite clause grammar expansion.
2
+ // Grammar rules are lowered to ordinary clauses during program preparation;
3
+ // phrase/2-3 use the same body expansion for dynamically supplied grammars.
4
+ import {
5
+ ATOM, COMPOUND, VAR, Env, atom, compound, deref, emptyList,
6
+ flattenConjunction, variable,
7
+ } from './term.js';
8
+ import { PrologError } from './iso.js';
9
+
10
+ let dcgFresh = 0;
11
+
12
+ function freshDcgVariable(label = 'S') {
13
+ // NUL cannot occur in a source variable name, so generated difference-list
14
+ // variables cannot capture a variable written by the program.
15
+ return variable(`\u0000dcg${++dcgFresh}:${label}`);
16
+ }
17
+
18
+ function conjunction(first, second) {
19
+ return compound(',', [first, second]);
20
+ }
21
+
22
+ function equality(left, right) {
23
+ return compound('=', [left, right]);
24
+ }
25
+
26
+ function listWithTail(items, tail) {
27
+ let list = tail;
28
+ for (let index = items.length - 1; index >= 0; index--) {
29
+ list = compound('.', [items[index], list]);
30
+ }
31
+ return list;
32
+ }
33
+
34
+ function terminalItems(term, env = new Env()) {
35
+ const items = [];
36
+ const original = term;
37
+ const seen = new Set();
38
+ let cursor = deref(term, env);
39
+ while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
40
+ if (seen.has(cursor)) throw new PrologError('type_error(terminal_sequence)', original);
41
+ seen.add(cursor);
42
+ items.push(cursor.args[0]);
43
+ cursor = deref(cursor.args[1], env);
44
+ }
45
+ if (cursor.type === ATOM && cursor.name === '[]') return items;
46
+ if (cursor.type === VAR) throw new PrologError('instantiation_error');
47
+ throw new PrologError('type_error(terminal_sequence)', original);
48
+ }
49
+
50
+ function terminalsGoal(terminals, input, output, env) {
51
+ return equality(input, listWithTail(terminalItems(terminals, env), output));
52
+ }
53
+
54
+ function appendStateArguments(nonterminal, input, output, module) {
55
+ nonterminal = deref(nonterminal, new Env());
56
+ if (nonterminal.type === ATOM) {
57
+ const goal = compound(nonterminal.name, [input, output]);
58
+ goal.module = module;
59
+ return goal;
60
+ }
61
+ if (nonterminal.type !== COMPOUND) throw new PrologError('type_error(callable)', nonterminal);
62
+ if (nonterminal.name === ':' && nonterminal.arity === 2) {
63
+ const qualifier = nonterminal.args[0];
64
+ if (qualifier.type === VAR) throw new PrologError('instantiation_error');
65
+ if (qualifier.type !== ATOM) throw new PrologError('type_error(atom)', qualifier);
66
+ return compound(':', [qualifier, appendStateArguments(nonterminal.args[1], input, output, qualifier.name)]);
67
+ }
68
+ const goal = compound(nonterminal.name, [...nonterminal.args, input, output]);
69
+ goal.module = nonterminal.module ?? module;
70
+ return goal;
71
+ }
72
+
73
+ function markGoalModule(term, module) {
74
+ if (!term || (term.type !== ATOM && term.type !== COMPOUND)) return term;
75
+ term.module ??= module;
76
+ if (term.type === COMPOUND &&
77
+ [',', ';', '->', '\\+', 'call', 'once', 'catch', 'phrase'].includes(term.name)) {
78
+ for (const argument of term.args) markGoalModule(argument, module);
79
+ }
80
+ return term;
81
+ }
82
+
83
+ function isTerminalSequence(term) {
84
+ return (term.type === ATOM && term.name === '[]') ||
85
+ (term.type === COMPOUND && term.name === '.' && term.arity === 2);
86
+ }
87
+
88
+ export function expandDcgBody(body, input, output, options = {}) {
89
+ const env = options.env ?? new Env();
90
+ const module = options.module ?? 'user';
91
+ body = deref(body, env);
92
+
93
+ if (body.type === VAR) {
94
+ const goal = compound('phrase', [body, input, output]);
95
+ goal.module = module;
96
+ return goal;
97
+ }
98
+
99
+ // Empty braces are the empty embedded-goal sequence. Like {true}, they
100
+ // consume no terminals and leave the grammar state unchanged.
101
+ if (body.type === ATOM && body.name === '{}') return equality(input, output);
102
+
103
+ if (isTerminalSequence(body)) return terminalsGoal(body, input, output, env);
104
+
105
+ if (body.type === COMPOUND && body.name === ',' && body.arity === 2) {
106
+ const middle = freshDcgVariable('sequence');
107
+ return conjunction(
108
+ expandDcgBody(body.args[0], input, middle, options),
109
+ expandDcgBody(body.args[1], middle, output, options),
110
+ );
111
+ }
112
+
113
+ if (body.type === COMPOUND && [';', '|'].includes(body.name) && body.arity === 2) {
114
+ const left = body.args[0];
115
+ if (body.name === ';' && left.type === COMPOUND && left.name === '->' && left.arity === 2) {
116
+ const middle = freshDcgVariable('condition');
117
+ return compound(';', [
118
+ compound('->', [
119
+ expandDcgBody(left.args[0], input, middle, options),
120
+ expandDcgBody(left.args[1], middle, output, options),
121
+ ]),
122
+ expandDcgBody(body.args[1], input, output, options),
123
+ ]);
124
+ }
125
+ return compound(';', [
126
+ expandDcgBody(left, input, output, options),
127
+ expandDcgBody(body.args[1], input, output, options),
128
+ ]);
129
+ }
130
+
131
+ if (body.type === COMPOUND && body.name === '{}' && body.arity === 1) {
132
+ return conjunction(markGoalModule(body.args[0], module), equality(input, output));
133
+ }
134
+
135
+ if (body.type === COMPOUND && body.name === 'call' && body.arity === 1) {
136
+ const closure = body.args[0];
137
+ if (closure.type === ATOM || closure.type === COMPOUND) closure.module ??= module;
138
+ const goal = compound('call', [closure, input, output]);
139
+ goal.module = module;
140
+ return goal;
141
+ }
142
+
143
+ if (body.type === COMPOUND && body.name === 'phrase' && body.arity === 1) {
144
+ const goal = compound('phrase', [body.args[0], input, output]);
145
+ goal.module = module;
146
+ return goal;
147
+ }
148
+
149
+ if (body.type === ATOM && body.name === '!') {
150
+ return conjunction(body, equality(input, output));
151
+ }
152
+
153
+ // TS 13211-3 leaves \+//1 and ->//2 implementation dependent. EyeProlog
154
+ // uses the widespread non-consuming negation and state-threading if-then
155
+ // definitions and documents these choices in its conformance profile.
156
+ if (body.type === COMPOUND && body.name === '\\+' && body.arity === 1) {
157
+ const ignored = freshDcgVariable('negated');
158
+ return conjunction(
159
+ compound('\\+', [expandDcgBody(body.args[0], input, ignored, options)]),
160
+ equality(input, output),
161
+ );
162
+ }
163
+
164
+ if (body.type === COMPOUND && body.name === '->' && body.arity === 2) {
165
+ const middle = freshDcgVariable('condition');
166
+ return compound('->', [
167
+ expandDcgBody(body.args[0], input, middle, options),
168
+ expandDcgBody(body.args[1], middle, output, options),
169
+ ]);
170
+ }
171
+
172
+ if (body.type !== ATOM && body.type !== COMPOUND) {
173
+ throw new PrologError('type_error(callable)', body);
174
+ }
175
+ return appendStateArguments(body, input, output, module);
176
+ }
177
+
178
+ function splitGrammarHead(head) {
179
+ let terminals = null;
180
+ if (head.type === COMPOUND && head.name === ',' && head.arity === 2) {
181
+ terminals = head.args[1];
182
+ head = head.args[0];
183
+ }
184
+ let module = null;
185
+ if (head.type === COMPOUND && head.name === ':' && head.arity === 2) {
186
+ if (head.args[0].type === VAR) throw new PrologError('instantiation_error');
187
+ if (head.args[0].type !== ATOM) throw new PrologError('type_error(atom)', head.args[0]);
188
+ module = head.args[0].name;
189
+ head = head.args[1];
190
+ }
191
+ if (head.type === VAR) throw new PrologError('instantiation_error');
192
+ if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
193
+ if (isTerminalSequence(head) ||
194
+ (head.type === COMPOUND && [',', ';', '|', '->', '{}'].includes(head.name)) ||
195
+ (head.type === ATOM && ['!', '{}'].includes(head.name))) {
196
+ throw new PrologError('permission_error(define, grammar_rule)', head);
197
+ }
198
+ return { head, terminals, module };
199
+ }
200
+
201
+ export function expandDcgRuleClause(clause, defaultModule = 'user') {
202
+ if (clause?.body?.length !== 0 || clause?.head?.type !== COMPOUND ||
203
+ clause.head.name !== '-->' || clause.head.arity !== 2) return null;
204
+
205
+ const declaration = splitGrammarHead(clause.head.args[0]);
206
+ const module = declaration.module ?? defaultModule;
207
+ const input = freshDcgVariable('input');
208
+ const output = freshDcgVariable('output');
209
+ const expandedHead = appendStateArguments(declaration.head, input, output, module);
210
+ delete expandedHead.module;
211
+
212
+ let bodyOutput = output;
213
+ if (declaration.terminals != null) bodyOutput = freshDcgVariable('semicontext');
214
+ let expandedBody = expandDcgBody(clause.head.args[1], input, bodyOutput, { module });
215
+ if (declaration.terminals != null) {
216
+ expandedBody = conjunction(
217
+ expandedBody,
218
+ terminalsGoal(declaration.terminals, output, bodyOutput, new Env()),
219
+ );
220
+ }
221
+
222
+ const expanded = {
223
+ head: expandedHead,
224
+ body: flattenConjunction(expandedBody),
225
+ module,
226
+ grammarRule: clause.head,
227
+ };
228
+ if (clause.source) expanded.source = clause.source;
229
+ return expanded;
230
+ }
231
+
232
+ export function isListOrPartialList(term, env) {
233
+ const seen = new Set();
234
+ let cursor = deref(term, env);
235
+ while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
236
+ if (seen.has(cursor)) return false;
237
+ seen.add(cursor);
238
+ cursor = deref(cursor.args[1], env);
239
+ }
240
+ return cursor.type === VAR || (cursor.type === ATOM && cursor.name === '[]');
241
+ }
242
+
243
+ export function emptyTerminalSequence() {
244
+ return emptyList();
245
+ }
package/src/iso.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from './term.js';
8
8
  import { createParserOperatorState, parseClauses } from './parser.js';
9
9
  import { formatTermForWrite } from './write.js';
10
+ import { emptyTerminalSequence, expandDcgBody, isListOrPartialList } from './dcg.js';
10
11
 
11
12
  let isoFresh = 0;
12
13
 
@@ -150,6 +151,8 @@ export const isoBuiltins = {
150
151
  registry.add('repeat', 0, repeatBuiltin);
151
152
  registry.add(';', 2, disjunctionBuiltin);
152
153
  registry.add('->', 2, ifThenBuiltin);
154
+ registry.add('phrase', 2, phraseBuiltin);
155
+ registry.add('phrase', 3, phraseBuiltin);
153
156
 
154
157
  registry.add('is', 2, isBuiltin, { deterministic: true });
155
158
  registry.add('=:=', 2, arithmeticComparison((n) => n === 0), { deterministic: true });
@@ -1545,6 +1548,38 @@ function* callClosureBuiltin({ solver, goal, env }) {
1545
1548
  solver.absorbStatsFrom(child);
1546
1549
  }
1547
1550
  }
1551
+
1552
+ function* phraseBuiltin({ solver, goal, env }) {
1553
+ const grammarBody = deref(goal.args[0], env);
1554
+ if (grammarBody.type === VAR) throw new PrologError('instantiation_error');
1555
+ if (grammarBody.type !== ATOM && grammarBody.type !== COMPOUND) {
1556
+ throw new PrologError('type_error(callable)', grammarBody);
1557
+ }
1558
+
1559
+ const input = goal.args[1];
1560
+ const requestedOutput = goal.arity === 2 ? emptyTerminalSequence() : goal.args[2];
1561
+ if (!isListOrPartialList(input, env)) {
1562
+ throw new PrologError('type_error(terminal_sequence)', deref(input, env));
1563
+ }
1564
+ if (!isListOrPartialList(requestedOutput, env)) {
1565
+ throw new PrologError('type_error(terminal_sequence)', deref(requestedOutput, env));
1566
+ }
1567
+
1568
+ // Delay the final output unification to keep phrase/3 steadfast in its
1569
+ // third argument, as required by the Part 3 execution model.
1570
+ const finalOutput = variable(`\u0000phrase:${++isoFresh}`);
1571
+ const expanded = expandDcgBody(grammarBody, input, finalOutput, {
1572
+ env,
1573
+ module: goal.module ?? grammarBody.module ?? 'user',
1574
+ });
1575
+ const finish = compound('=', [finalOutput, requestedOutput]);
1576
+ const child = solver.cloneForInnerGoal();
1577
+ try {
1578
+ yield* child.solve([expanded, finish], env, 0);
1579
+ } finally {
1580
+ solver.absorbStatsFrom(child);
1581
+ }
1582
+ }
1548
1583
  function formalErrorTerm(error) {
1549
1584
  const parse = (text) => {
1550
1585
  const open = text.indexOf('(');
@@ -1625,7 +1660,14 @@ function* disjunctionBuiltin({ solver, goal, env }) {
1625
1660
  yield* solver.solve([callable(goal.args[1], env)], env.clone(), 0);
1626
1661
  return;
1627
1662
  }
1663
+ const marker = solver.active[solver.active.length - 1] ?? null;
1664
+ const markerCutEpoch = marker?.cutEpoch ?? 0;
1665
+ const solverCutEpoch = solver.cutEpoch;
1628
1666
  yield* solver.solve([callable(goal.args[0], env)], env.clone(), 0);
1667
+ const cutThisScope = marker == null
1668
+ ? solver.cutEpoch !== solverCutEpoch
1669
+ : (marker.cutEpoch ?? 0) !== markerCutEpoch;
1670
+ if (cutThisScope) return;
1629
1671
  yield* solver.solve([callable(goal.args[1], env)], env.clone(), 0);
1630
1672
  }
1631
1673
  function* ifThenBuiltin({ solver, goal, env }) {
package/src/parser.js CHANGED
@@ -39,6 +39,8 @@ const graphicAtomChars = '#$&*+-./<=>@^~\\;:';
39
39
  // canonical notation. Commas remain separators except inside parentheses.
40
40
  const INFIX_OPERATORS = new Map([
41
41
  [':-', { precedence: 2, associativity: 'none' }],
42
+ ['-->', { precedence: 2, associativity: 'none' }],
43
+ ['|', { precedence: 4, associativity: 'right' }],
42
44
  [';', { precedence: 5, associativity: 'right' }],
43
45
  ['->', { precedence: 7, associativity: 'right' }],
44
46
  [',', { precedence: 10, associativity: 'right' }],
@@ -82,7 +84,8 @@ const PREFIX_OPERATORS = new Map([
82
84
  ]);
83
85
 
84
86
  export const ISO_OPERATOR_DEFINITIONS = [
85
- [1200, 'xfx', ':-'], [1200, 'fx', ':-'],
87
+ [1200, 'xfx', ':-'], [1200, 'fx', ':-'], [1200, 'xfx', '-->'],
88
+ [1105, 'xfy', '|'],
86
89
  [1100, 'xfy', ';'], [1050, 'xfy', '->'], [1000, 'xfy', ','],
87
90
  [900, 'fy', '\\+'],
88
91
  ...['=', '=..', '\\=', '==', '\\==', '@<', '@=<', '@>', '@>=', 'is',
@@ -618,7 +621,25 @@ class Parser {
618
621
  accept(clause);
619
622
  continue;
620
623
  }
621
- const head = this.parseTerm(3);
624
+ let head = this.parseTerm(3);
625
+ // ISO/IEC TS 13211-3 grammar rules use -->/2 at the same top-level
626
+ // priority as clauses. The left side may include an unparenthesized
627
+ // semicontext: NonTerminal, Terminals --> Body.
628
+ if (this.token.type === TOK.COMMA) {
629
+ this.advance();
630
+ head = compound(',', [head, this.parseTerm(3)]);
631
+ }
632
+ if (this.operatorTokenName() === '-->') {
633
+ this.advance();
634
+ const grammarBody = this.parseTerm(0, true);
635
+ this.expect(TOK.DOT, '.');
636
+ this.advance();
637
+ const clause = { head: compound('-->', [head, grammarBody]), body: [] };
638
+ clauseNumber++;
639
+ if (this.sourceMetadata) clause.source = { filename: this.filename, line, clause: clauseNumber };
640
+ accept(clause);
641
+ continue;
642
+ }
622
643
  const body = [];
623
644
  if (this.token.type === TOK.IF) {
624
645
  this.advance();
package/src/program.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import { PrologError } from './iso.js';
12
12
  import { currentWorkingDirectory, fs, path } from './platform.js';
13
13
  import { standardLibrarySources } from './standard-library.js';
14
+ import { expandDcgRuleClause } from './dcg.js';
14
15
 
15
16
  const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
16
17
  const FAST_PARSE_ABORT = Symbol('fastParseAbort');
@@ -663,6 +664,8 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
663
664
  batch.length = 0;
664
665
  };
665
666
  const accept = (clause) => {
667
+ const grammarClause = expandDcgRuleClause(clause, context.module);
668
+ if (grammarClause) clause = grammarClause;
666
669
  const moduleDeclaration = moduleDirective(clause);
667
670
  if (moduleDeclaration) {
668
671
  flush();
@@ -690,9 +693,9 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
690
693
  }
691
694
  const include = includeDirective(clause);
692
695
  if (!include) {
693
- clause.module = context.module;
696
+ clause.module ??= context.module;
694
697
  if (!isDirectiveClause(clause)) {
695
- for (const goal of clause.body) annotateGoalModule(goal, context.module);
698
+ for (const goal of clause.body) annotateGoalModule(goal, clause.module);
696
699
  }
697
700
  batch.push(clause);
698
701
  if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
@@ -759,9 +762,14 @@ function moduleExportIndicators(term) {
759
762
  if (items == null) return null;
760
763
  const indicators = [];
761
764
  for (const item of items) {
762
- if (item.type !== COMPOUND || item.name !== '/' || item.arity !== 2) return null;
765
+ if (item.type !== COMPOUND || !['/', '//'].includes(item.name) || item.arity !== 2) return null;
763
766
  const indicator = predicateIndicator(item.args[0], item.args[1]);
764
767
  if (!indicator) return null;
768
+ if (item.name === '//') {
769
+ indicator.arity += 2;
770
+ indicator.key = `${indicator.name}/${indicator.arity}`;
771
+ indicator.nonterminalArity = indicator.arity - 2;
772
+ }
765
773
  indicators.push(indicator);
766
774
  }
767
775
  return indicators;
@@ -844,12 +852,21 @@ function dynamicDirectiveIndicators(clause) {
844
852
  if (directive.type !== COMPOUND || directive.name !== 'dynamic' || directive.arity !== 1) return [];
845
853
  const terms = properListItems(directive.args[0], new Env()) ?? flattenDirectiveSequence(directive.args[0]);
846
854
  return terms.map((indicator) =>
847
- indicator.type === COMPOUND && indicator.name === '/' && indicator.arity === 2
848
- ? predicateIndicator(indicator.args[0], indicator.args[1])
855
+ indicator.type === COMPOUND && ['/', '//'].includes(indicator.name) && indicator.arity === 2
856
+ ? nonterminalOrPredicateIndicator(indicator)
849
857
  : null
850
858
  ).filter(Boolean);
851
859
  }
852
860
 
861
+ function nonterminalOrPredicateIndicator(term) {
862
+ const indicator = predicateIndicator(term.args[0], term.args[1]);
863
+ if (!indicator || term.name !== '//') return indicator;
864
+ indicator.arity += 2;
865
+ indicator.key = `${indicator.name}/${indicator.arity}`;
866
+ indicator.nonterminalArity = indicator.arity - 2;
867
+ return indicator;
868
+ }
869
+
853
870
  function flattenDirectiveSequence(term) {
854
871
  if (term.type === COMPOUND && term.name === ',' && term.arity === 2) {
855
872
  return [...flattenDirectiveSequence(term.args[0]), ...flattenDirectiveSequence(term.args[1])];
package/src/solver.js CHANGED
@@ -61,6 +61,7 @@ export class Solver {
61
61
  this.io = options.io ?? new StreamManager(options.ioOptions);
62
62
  this.solveStacks = [];
63
63
  this.active = [];
64
+ this.cutEpoch = 0;
64
65
  this.memo = new Map();
65
66
  this.tableCoordinator = null;
66
67
  this.groundChainSuccess = new Set();
@@ -215,10 +216,12 @@ export class Solver {
215
216
  // be run early as pure filters. Stop at internal sentinels so rule-body
216
217
  // active guards are released before the caller's remaining goals are seen.
217
218
  const selectedIndex = selectReadyDeterministicBuiltin(goals, env, this.registry);
218
- const goal = goals[selectedIndex];
219
+ const goal = deref(goals[selectedIndex], env);
219
220
  const rest = selectedIndex === 0 ? goals.slice(1) : [...goals.slice(0, selectedIndex), ...goals.slice(selectedIndex + 1)];
220
221
  if (goal.type === 'atom' && goal.name === '!' && goal.arity === 0) {
221
222
  const marker = active[active.length - 1] ?? null;
223
+ this.cutEpoch++;
224
+ if (marker) marker.cutEpoch = (marker.cutEpoch ?? 0) + 1;
222
225
  for (const solveStack of this.solveStacks) {
223
226
  for (let i = solveStack.length - 1; i >= 0; i--) {
224
227
  if (marker == null || solveStack[i].active?.includes(marker)) solveStack.splice(i, 1);
@@ -247,7 +250,9 @@ export class Solver {
247
250
  continue;
248
251
  }
249
252
 
253
+ if (goal.type === 'var') throw new PrologError('instantiation_error');
250
254
  const callable = goal.type === COMPOUND || goal.type === 'atom';
255
+ if (!callable) throw new PrologError('type_error(callable)', goal);
251
256
  const def = callable ? this.registry.get(goal.name, goal.arity) : null;
252
257
  this.active = active;
253
258
  if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
@@ -274,7 +279,6 @@ export class Solver {
274
279
  }
275
280
 
276
281
  this.stats.solve_one_goal_calls++;
277
- if (!callable) break;
278
282
  const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
279
283
  if (!group) {
280
284
  if (this.prologFlags.get('unknown')?.value?.name === 'error') {
@@ -1,4 +1,4 @@
1
- # ISO/IEC 13211-1 conformance matrix
1
+ # ISO Prolog conformance matrix
2
2
 
3
3
  The normative baseline for the `iso/` corpus is ISO/IEC 13211-1:1995 plus
4
4
  Technical Corrigenda 1:2007, 2:2012, and 3:2017. This matrix links the standard
@@ -18,6 +18,8 @@ case totals.
18
18
  | 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
19
  | 8.17 flags and hooks | required flags, mutation permissions, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases |
20
20
  | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power` |
21
+ | ISO/IEC 13211-2 modules | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
22
+ | ISO/IEC TS 13211-3 grammar rules | `-->`, terminal and partial sequences, grammar control constructs, semicontexts, nonterminal indicators, modules, `phrase/2-3`, steadfastness and errors | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_partial_sequences`, `dcg_phrase_steadfastness`, `dcg_dynamic_nonterminal_indicator`, `logtalk_dcg_phrase_identity`, `logtalk_dcg_semicontexts`, DCG error and precedence cases |
21
23
 
22
24
  Corrigendum-specific coverage includes double-quoted atom operator priority
23
25
  (Cor.1); the added predicates and evaluable functors, bar-operator rules,
@@ -7,13 +7,15 @@ supported ISO Prolog profile, built-ins, extensions, and reasoner behavior.
7
7
 
8
8
  The suite is intentionally file-based. Exact standard output, errors, warnings,
9
9
  and proof output test the behavior of the JavaScript implementation.
10
- [ISO-MATRIX.md](ISO-MATRIX.md) maps normative clause families and all three
11
- corrigenda to representative executable cases.
10
+ [ISO-MATRIX.md](ISO-MATRIX.md) maps Part 1 normative clause families, all three
11
+ corrigenda, Part 2 modules, and Part 3 definite clause grammars to representative
12
+ executable cases.
12
13
 
13
14
  “Conformance” here means conformance to EyeProlog's documented ISO compatibility
14
15
  profile and implementation extensions. The default registry covers the exact
15
16
  predicate indicators listed in Appendix B of the book across the supported
16
- ISO/IEC 13211-1:1995 standard families. This suite is still not an independent certification of
17
+ ISO/IEC 13211-1:1995, ISO/IEC 13211-2:2000, and ISO/IEC TS 13211-3:2025
18
+ standard families. This suite is still not an independent certification of
17
19
  every processor requirement, lexical edge, option combination, or prescribed
18
20
  error precedence. Cases under `iso/` identify standards-derived behavior;
19
21
  other directories cover EyeProlog host contracts and extensions.
@@ -88,11 +90,11 @@ ISO/IEC 13211-1 clauses 7 and 8. In particular, isolated negative cases keep
88
90
  instantiation, type, domain, permission, representation, and evaluation errors
89
91
  independently observable.
90
92
 
91
- Selected cases are adapted from the ISO and standard-core suites of Scryer
92
- Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
93
+ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
94
+ Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
93
95
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
94
96
 
95
- The corpus has 304 cases in `iso/` and 710 file-based conformance cases in
97
+ The corpus has 373 cases in `iso/` and 779 file-based conformance cases in
96
98
  total. The generated `conformance-report.md` is the authoritative source for
97
99
  current category totals. Together with regression, documentation-sync, API,
98
100
  example, and book-example checks, `npm test` is the release gate.