eyeprolog 1.5.50 → 1.5.52

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 (41) hide show
  1. package/conformance-report.md +2 -2
  2. package/examples/book/README.md +18 -20
  3. package/examples/book/chapter-03/09-program.pl +1 -1
  4. package/examples/book/chapter-39/01-elk.pl +6 -0
  5. package/examples/book/chapter-39/02-solve.pl +10 -0
  6. package/package.json +1 -1
  7. package/src/iso.js +14 -1
  8. package/src/parser.js +1 -1
  9. package/src/program.js +39 -10
  10. package/src/solver.js +10 -1
  11. package/test/conformance/ISO-IMPLEMENTATION-DEFINED.md +2 -1
  12. package/test/conformance/ISO-PROLOG-TEXT-EXECUTION-MATRIX.md +2 -2
  13. package/test/conformance/README.md +1 -1
  14. package/test/conformance/cases/iso/public_procedure_access.pl +52 -0
  15. package/test/conformance/errors/iso/consult_redefines_builtin.pl +11 -0
  16. package/test/conformance/expected/iso/exceptions_and_flags.pl +1 -1
  17. package/test/conformance/expected/iso/public_procedure_access.pl +6 -0
  18. package/test/conformance/expected-errors/iso/consult_redefines_builtin.txt +1 -0
  19. package/test/run-regression.mjs +193 -5
  20. package/the-art-of-eyeprolog.md +62 -5
  21. package/tools/extract-book-examples.mjs +28 -0
  22. package/examples/book/chapter-08/01-findall.pl +0 -8
  23. package/examples/book/chapter-09/01-functor.pl +0 -4
  24. package/examples/book/chapter-24/03-reachable.pl +0 -6
  25. package/examples/book/chapter-28/01-program.pl +0 -2
  26. /package/examples/book/chapter-08/{02-outgoing_costs.pl → 01-outgoing_costs.pl} +0 -0
  27. /package/examples/book/chapter-08/{03-best_route.pl → 02-best_route.pl} +0 -0
  28. /package/examples/book/chapter-09/{02-normalized.pl → 01-normalized.pl} +0 -0
  29. /package/examples/book/chapter-09/{03-message.pl → 02-message.pl} +0 -0
  30. /package/examples/book/chapter-24/{04-best_plan.pl → 03-best_plan.pl} +0 -0
  31. /package/examples/book/chapter-28/{02-triangle.pl → 01-triangle.pl} +0 -0
  32. /package/examples/book/chapter-28/{03-rectangle.pl → 02-rectangle.pl} +0 -0
  33. /package/examples/book/chapter-28/{04-integer_rectangle.pl → 03-integer_rectangle.pl} +0 -0
  34. /package/examples/book/chapter-28/{05-preserves_combine.pl → 04-preserves_combine.pl} +0 -0
  35. /package/examples/book/chapter-39/{01-answer.pl → 03-answer.pl} +0 -0
  36. /package/examples/book/chapter-39/{02-answer-2.pl → 04-answer-2.pl} +0 -0
  37. /package/examples/book/chapter-39/{03-answer-3.pl → 05-answer-3.pl} +0 -0
  38. /package/examples/book/chapter-39/{04-cost.pl → 06-cost.pl} +0 -0
  39. /package/examples/book/chapter-39/{05-message.pl → 07-message.pl} +0 -0
  40. /package/examples/book/chapter-39/{06-task.pl → 08-task.pl} +0 -0
  41. /package/examples/book/chapter-39/{07-program.pl → 09-program.pl} +0 -0
@@ -32,7 +32,7 @@ spelling therefore changes this report even when no corpus file is added or remo
32
32
  | context | 11 | 0 | 0 | 0 | 11 |
33
33
  | control | 15 | 0 | 0 | 0 | 15 |
34
34
  | explicit-tabling | 6 | 0 | 0 | 0 | 6 |
35
- | iso | 173 | 218 | 0 | 0 | 391 |
35
+ | iso | 174 | 219 | 0 | 0 | 393 |
36
36
  | lists | 52 | 3 | 0 | 0 | 55 |
37
37
  | modules | 2 | 0 | 0 | 0 | 2 |
38
38
  | negation | 8 | 0 | 19 | 0 | 27 |
@@ -45,7 +45,7 @@ spelling therefore changes this report even when no corpus file is added or remo
45
45
  | terms | 26 | 3 | 0 | 0 | 29 |
46
46
  | unification | 18 | 0 | 0 | 0 | 18 |
47
47
  | variables | 16 | 7 | 0 | 0 | 23 |
48
- | **Total** | **495** | **273** | **19** | **21** | **808** |
48
+ | **Total** | **496** | **274** | **19** | **21** | **810** |
49
49
 
50
50
  ## DCG conformance clarification
51
51
 
@@ -61,15 +61,13 @@ npm run generate
61
61
 
62
62
  ## Chapter 8: Collecting and choosing answers
63
63
 
64
- - [01-findall.pl](chapter-08/01-findall.pl)
65
- - [02-outgoing_costs.pl](chapter-08/02-outgoing_costs.pl)
66
- - [03-best_route.pl](chapter-08/03-best_route.pl)
64
+ - [01-outgoing_costs.pl](chapter-08/01-outgoing_costs.pl)
65
+ - [02-best_route.pl](chapter-08/02-best_route.pl)
67
66
 
68
67
  ## Chapter 9: Structured data, text, and contexts
69
68
 
70
- - [01-functor.pl](chapter-09/01-functor.pl)
71
- - [02-normalized.pl](chapter-09/02-normalized.pl)
72
- - [03-message.pl](chapter-09/03-message.pl)
69
+ - [01-normalized.pl](chapter-09/01-normalized.pl)
70
+ - [02-message.pl](chapter-09/02-message.pl)
73
71
 
74
72
  ## Chapter 10: From puzzles to models
75
73
 
@@ -148,8 +146,7 @@ npm run generate
148
146
 
149
147
  - [01-worker.pl](chapter-24/01-worker.pl) — Generate, constrain, describe
150
148
  - [02-simple_path.pl](chapter-24/02-simple_path.pl) — Search over states
151
- - [03-reachable.pl](chapter-24/03-reachable.pl) — Existence, one witness, and all witnesses
152
- - [04-best_plan.pl](chapter-24/04-best_plan.pl) — Optimization is search plus an order
149
+ - [03-best_plan.pl](chapter-24/03-best_plan.pl) — Optimization is search plus an order
153
150
 
154
151
  ## Chapter 25: Case study: an auditable decision service
155
152
 
@@ -174,11 +171,10 @@ npm run generate
174
171
 
175
172
  ## Chapter 28: Algebra, symmetry, and representation
176
173
 
177
- - [01-program.pl](chapter-28/01-program.pl) — Unification is structural equation solving
178
- - [02-triangle.pl](chapter-28/02-triangle.pl) — Symmetry reduces search
179
- - [03-rectangle.pl](chapter-28/03-rectangle.pl) — Relations reveal inverse problems
180
- - [04-integer_rectangle.pl](chapter-28/04-integer_rectangle.pl)
181
- - [05-preserves_combine.pl](chapter-28/05-preserves_combine.pl) — Composition, homomorphism, and reusable laws
174
+ - [01-triangle.pl](chapter-28/01-triangle.pl) — Symmetry reduces search
175
+ - [02-rectangle.pl](chapter-28/02-rectangle.pl) — Relations reveal inverse problems
176
+ - [03-integer_rectangle.pl](chapter-28/03-integer_rectangle.pl)
177
+ - [04-preserves_combine.pl](chapter-28/04-preserves_combine.pl) — Composition, homomorphism, and reusable laws
182
178
 
183
179
  ## Chapter 29: Search as experimental mathematics
184
180
 
@@ -238,13 +234,15 @@ npm run generate
238
234
 
239
235
  ## Chapter 39: Predicate reference
240
236
 
241
- - [01-answer.pl](chapter-39/01-answer.pl) — Library relations by programming role
242
- - [02-answer-2.pl](chapter-39/02-answer-2.pl)
243
- - [03-answer-3.pl](chapter-39/03-answer-3.pl)
244
- - [04-cost.pl](chapter-39/04-cost.pl)
245
- - [05-message.pl](chapter-39/05-message.pl)
246
- - [06-task.pl](chapter-39/06-task.pl)
247
- - [07-program.pl](chapter-39/07-program.pl) — Specialized library implementation notes
237
+ - [01-elk.pl](chapter-39/01-elk.pl) — Reading static procedures
238
+ - [02-solve.pl](chapter-39/02-solve.pl)
239
+ - [03-answer.pl](chapter-39/03-answer.pl) — Library relations by programming role
240
+ - [04-answer-2.pl](chapter-39/04-answer-2.pl)
241
+ - [05-answer-3.pl](chapter-39/05-answer-3.pl)
242
+ - [06-cost.pl](chapter-39/06-cost.pl)
243
+ - [07-message.pl](chapter-39/07-message.pl)
244
+ - [08-task.pl](chapter-39/08-task.pl)
245
+ - [09-program.pl](chapter-39/09-program.pl) — Specialized library implementation notes
248
246
 
249
247
  ## Chapter 40: Running EyeProlog: command line and corpus
250
248
 
@@ -1,2 +1,2 @@
1
1
  % From The Art of EyeProlog, Chapter 3 — Equality, unification, and the occurs check.
2
- (X = wrapper(X)).
2
+ cyclic_unification :- X = wrapper(X).
@@ -0,0 +1,6 @@
1
+ % From The Art of EyeProlog, Chapter 39 — Reading static procedures.
2
+ :- public(elk/1).
3
+
4
+ elk(X) :- moose(X).
5
+
6
+ moose(bertha).
@@ -0,0 +1,10 @@
1
+ % From The Art of EyeProlog, Chapter 39.
2
+ :- set_prolog_flag(default_procedure_access, public).
3
+
4
+ solve(true) :- !.
5
+ solve((A, B)) :- !, solve(A), solve(B).
6
+ solve(H) :- clause(H, Body), solve(Body).
7
+
8
+ elk(X) :- moose(X).
9
+ moose(bertha).
10
+ grazes(X) :- elk(X).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.50",
6
+ "version": "1.5.52",
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
@@ -740,7 +740,13 @@ function* clauseSolutions({ solver, goal, env }, state) {
740
740
  // the default in every mode, not only under --iso-strict: assert/1,
741
741
  // retract/1, and abolish/1 already reject static procedures unconditionally,
742
742
  // and clause/2 now agrees with them.
743
- if (isProcessorStaticProcedure(solver, head) || (group && !group.dynamic)) {
743
+ //
744
+ // 7.5.3 notes that a public/1 directive would be an extension, so a static
745
+ // procedure declared public, or any user-defined procedure when the
746
+ // default_procedure_access flag is public, stays readable here. Either way
747
+ // the procedure remains static and cannot be modified.
748
+ if (isProcessorStaticProcedure(solver, head) ||
749
+ (group && !group.dynamic && !isPublicProcedure(solver, group))) {
744
750
  throw new PrologError('permission_error(access, private_procedure)', indicator);
745
751
  }
746
752
  callableOrVariable(goal.args[1], env);
@@ -798,6 +804,13 @@ function isGrammarRuleProcedure(solver, head) {
798
804
  return !solver.isoStrict && head.name === '-->' && head.arity === 2;
799
805
  }
800
806
 
807
+ // A user-defined procedure is readable by clause/2 when it was declared public
808
+ // or when the processor grants access to every user-defined procedure.
809
+ function isPublicProcedure(solver, group) {
810
+ if (group.public) return true;
811
+ return solver.prologFlags?.get('default_procedure_access')?.value?.name === 'public';
812
+ }
813
+
801
814
  function isProcessorStaticProcedure(solver, head) {
802
815
  // Conjunction is an ISO control construct (7.5/Table 9) but is executed
803
816
  // directly by the solver rather than through the builtin registry. The
package/src/parser.js CHANGED
@@ -1130,7 +1130,7 @@ class Parser {
1130
1130
  (['char_conversion', 'set_prolog_flag'].includes(directive.name) && directive.arity === 2)
1131
1131
  );
1132
1132
  const extensionDirective = directive.type === 'compound' && (
1133
- (['use_module', 'meta_predicate', 'attribute', 'table'].includes(directive.name) && directive.arity === 1) ||
1133
+ (['use_module', 'meta_predicate', 'attribute', 'table', 'public'].includes(directive.name) && directive.arity === 1) ||
1134
1134
  (['module', 'use_module'].includes(directive.name) && directive.arity === 2)
1135
1135
  );
1136
1136
  if (this.strictIso && extensionDirective) {
package/src/program.js CHANGED
@@ -154,6 +154,10 @@ export class Program {
154
154
  this.libraryImports = [];
155
155
  this.interopPortabilityWarnings = [];
156
156
  this.dynamicPredicates = new Set();
157
+ // ISO 7.5.3 notes that a public/1 directive declaring user-defined
158
+ // procedures to be public would be an extension. Such a procedure stays
159
+ // static -- it cannot be modified -- but clause/2 may inspect it.
160
+ this.publicPredicates = new Set();
157
161
  this.multifilePredicates = new Set();
158
162
  this.discontiguousPredicates = new Set();
159
163
  this.strictIso = options.isoStrict === true;
@@ -221,6 +225,7 @@ export class Program {
221
225
  wfsDatalog: false,
222
226
  scalarFactsOnly: true,
223
227
  dynamic: this.dynamicPredicates.has(modulePredicateKey(module, name, arity)),
228
+ public: this.publicPredicates.has(modulePredicateKey(module, name, arity)),
224
229
  negationStratum: null,
225
230
  hasCut: false,
226
231
  cutReachable: null,
@@ -636,6 +641,7 @@ class ProgramBuilder {
636
641
  this.options = options;
637
642
  this.program = program ?? new Program([], { ...options, [DEFER_PROGRAM_BUILD]: true });
638
643
  this.declaredDynamicIndicators = new Map();
644
+ this.declaredPublicIndicators = new Map();
639
645
  this.declaredMultifileIndicators = new Map();
640
646
  this.declaredDiscontiguousIndicators = new Map();
641
647
  this.declaredTableIndicators = new Map();
@@ -651,7 +657,7 @@ class ProgramBuilder {
651
657
  const unit = textUnit ?? '<input>';
652
658
  let declarations = this.directiveDeclarationsByText.get(unit);
653
659
  if (!declarations) {
654
- declarations = { dynamic: new Set(), multifile: new Set(), discontiguous: new Set(), table: new Set() };
660
+ declarations = { dynamic: new Set(), public: new Set(), multifile: new Set(), discontiguous: new Set(), table: new Set() };
655
661
  this.directiveDeclarationsByText.set(unit, declarations);
656
662
  }
657
663
  return declarations[kind];
@@ -717,6 +723,13 @@ class ProgramBuilder {
717
723
  local.add(key);
718
724
  targetSet.add(key);
719
725
  declaredMap.set(`${textUnit}\u0000${key}`, { ...indicator, key, module, textUnit });
726
+ if (kind === 'public') {
727
+ // Unlike dynamic/1, a public declaration says nothing about existence:
728
+ // it only grants clause/2 access. Groups created later pick the flag up
729
+ // from program.publicPredicates in makeGroup.
730
+ const existing = program.groups.get(key);
731
+ if (existing) existing.public = true;
732
+ }
720
733
  if (kind === 'dynamic') {
721
734
  // A dynamic declaration creates the procedure immediately, not only
722
735
  // when ProgramBuilder.finish() runs. Compile-time source-expansion
@@ -830,6 +843,10 @@ class ProgramBuilder {
830
843
  const module = clause.module ?? 'user';
831
844
  const textUnit = clause.textUnit ?? '<input>';
832
845
  this.addProcedureDirective(clause, 'dynamic', program.dynamicPredicates, this.declaredDynamicIndicators);
846
+ // public/1 is an extension, so the strict Part 1 profile does not offer it.
847
+ if (!program.strictIso) {
848
+ this.addProcedureDirective(clause, 'public', program.publicPredicates, this.declaredPublicIndicators);
849
+ }
833
850
  this.addProcedureDirective(clause, 'multifile', program.multifilePredicates, this.declaredMultifileIndicators);
834
851
  this.addProcedureDirective(clause, 'discontiguous', program.discontiguousPredicates, this.declaredDiscontiguousIndicators);
835
852
  this.addProcedureDirective(clause, 'table', program.tabledPredicates, this.declaredTableIndicators);
@@ -1879,16 +1896,28 @@ function assertDynamicIndicatorIsDefinable(indicator, strictIso = false) {
1879
1896
 
1880
1897
  function assertPredicateIsDefinable(name, arity, strictIso = false) {
1881
1898
  // false/0 is standardized as a static built-in by Corrigendum 2 and cannot
1882
- // be redefined in either profile. Strict core mode extends the same ISO
1883
- // rule to every Part-1 built-in/control construct; the normal EyeProlog
1884
- // profile keeps its historical source-compatibility behavior.
1885
- const strictSyntaxProcedure = strictIso && (
1886
- (name === ',' && arity === 2) ||
1887
- (name === ':-' && (arity === 1 || arity === 2))
1888
- );
1899
+ // be redefined in either profile.
1900
+ //
1901
+ // ISO 7.4.3 requires that the predicate indicator of the head of a clause in
1902
+ // a Prolog text is not that of a built-in predicate or a control construct.
1903
+ // That subclause otherwise defers to assertz/1 and waives only the static
1904
+ // procedure error, so the built-in restriction is a separate requirement and
1905
+ // is not profile-dependent. The strict ISO registry holds exactly the Part 1
1906
+ // built-ins and control constructs, so it is consulted in every mode.
1907
+ //
1908
+ // EyeProlog's library and extension predicates are deliberately absent from
1909
+ // that registry and stay redefinable, which preserves the source
1910
+ // compatibility that user programs rely on.
1911
+ //
1912
+ // (,)/2 is a control construct (table 9) but is recognized by the parser
1913
+ // rather than the registry, so it is named here. (:-)/1-2 come from the STC
1914
+ // #56 working draft rather than published text and stay strict-only.
1915
+ const controlConstruct = name === ',' && arity === 2;
1916
+ const draftSyntaxProcedure = strictIso && name === ':-' && (arity === 1 || arity === 2);
1889
1917
  if ((name === 'false' && arity === 0) ||
1890
- (strictIso && getStrictIsoRegistry().get(name, arity)) ||
1891
- strictSyntaxProcedure) {
1918
+ getStrictIsoRegistry().get(name, arity) ||
1919
+ controlConstruct ||
1920
+ draftSyntaxProcedure) {
1892
1921
  throw staticProcedureModificationError(name, arity);
1893
1922
  }
1894
1923
  }
package/src/solver.js CHANGED
@@ -1263,8 +1263,17 @@ function defaultPrologFlags(unknown = 'error', strictIso = false) {
1263
1263
  ['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
1264
1264
  ['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
1265
1265
  ['occurs_check', { value: compound('true', []), allowed: ['true', 'error'], changeable: true }],
1266
+ // ISO 7.5.2 makes every user-defined procedure static, and therefore
1267
+ // private, unless declared otherwise. Setting this flag to public grants
1268
+ // clause/2 access to all of them at once, which meta-interpreters need
1269
+ // without having to enumerate every predicate in a public/1 directive.
1270
+ // The procedures stay static: assert/1 and retract/1 still refuse them.
1271
+ ['default_procedure_access', { value: compound('private', []), allowed: ['private', 'public'], changeable: true }],
1266
1272
  ]);
1267
- if (strictIso) flags.delete('occurs_check');
1273
+ if (strictIso) {
1274
+ flags.delete('occurs_check');
1275
+ flags.delete('default_procedure_access');
1276
+ }
1268
1277
  return flags;
1269
1278
  }
1270
1279
 
@@ -69,7 +69,7 @@ Status values are:
69
69
  | 7.11.2.5 | Default `double_quotes` | `chars`. | **defined** — `src/solver.js`, parser flag state. |
70
70
  | 7.12.1 | Second argument of `error/2` | The default context term is the atom `eyeprolog`. A few implementation-specific diagnostics may deliberately supply a more specific context term. | **defined** — `formalErrorTerm()` in `src/iso.js`. |
71
71
  | 7.12.2(f) | Implementation-defined representation limits | Character and character-code operations are limited to Unicode scalar values; surrogates and values above U+10FFFF are representation errors. Arity/integer values are modeled as unbounded but may hit host/resource limits. Float input overflow uses the implementation-specific `max_float`/`min_float` representation names documented by the STC-oriented tests. | **defined** — parser/ISO numeric and character guards. |
72
- | 8.17.1 | Implementation-defined flag value ranges | Strict mode exposes only Part 1 core flags and their standard value sets. Normal mode additionally exposes EyeProlog's `occurs_check` flag. With `bounded=false`, `max_integer` and `min_integer` have no current or selectable value and their `current_prolog_flag/2` queries fail. Valid alternative values of fixed standard flags are distinguished from invalid values so `set_prolog_flag/2` reports permission versus domain errors as prescribed. | **defined** — strict registry/flag filtering in `src/solver.js`; strict flag tests. |
72
+ | 8.17.1 | Implementation-defined flag value ranges | Strict mode exposes only Part 1 core flags and their standard value sets. Normal mode additionally exposes EyeProlog's `occurs_check` and `default_procedure_access` flags. With `bounded=false`, `max_integer` and `min_integer` have no current or selectable value and their `current_prolog_flag/2` queries fail. Valid alternative values of fixed standard flags are distinguished from invalid values so `set_prolog_flag/2` reports permission versus domain errors as prescribed. | **defined** — strict registry/flag filtering in `src/solver.js`; strict flag tests. |
73
73
  | 8.17.3 | Other effects of `halt/0` | Terminates EyeProlog execution and returns host/process status `0`; it produces no Prolog solution. | **defined** — `HaltSignal`, `haltBuiltin()`, CLI/runner handling. |
74
74
  | 8.17.4 | Meaning/effects of `halt(Status)` | Integer `Status` is converted to the host process/runner halt code; it produces no Prolog solution. | **defined** — `haltBuiltin()`, `src/execute.js`, `src/cli.js`. |
75
75
  | 9.1.4.1 | Floating-point rounding function `rndF` | Floating values and operations use ECMAScript `Number` (IEEE-754 binary64) and the host's specified binary64 arithmetic/conversions. | **defined** — `src/iso-arithmetic.js`, `src/number-value.js`. |
@@ -121,6 +121,7 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
121
121
  | 5.5.6 Side effects | Normal mode adds `statistics/0-2`, cleanup/library state, and optional proof/statistics host instrumentation. | The strict registry excludes Prolog-visible statistics/cleanup/library adapters; ordinary Part 1 I/O/database/flag/operator side effects remain. Host proof/statistics collection observes execution through the embedding API rather than adding a strict Prolog goal effect. **covered** — strict registry/execution regression. |
122
122
  | 5.5.7 Control constructs | `tnot/1`, `wfs_truth/2`, and normal-profile execution optimizations | `tnot/1`, `wfs_truth/2`, and the normal-profile explicit `table` declaration are absent. |
123
123
  | 5.5.8 Flags | `occurs_check` | Absent in strict mode. |
124
+ | 5.5.8 Flags, 7.5.3 NOTE | `public/1` directive, `default_procedure_access` flag | Absent in strict mode. The 7.5.3 NOTE anticipates a `public/1` extension declaring user-defined procedures public; the flag generalizes it to all of them at once, which meta-interpreters need without annotating the interpreted program. Read access only: a public procedure stays static, so `assert`/`retract` still report `permission_error(modify, static_procedure)`, and built-ins stay private. |
124
125
  | 5.5.9 Built-in predicates | EyeProlog libraries, CLP(Z), statistics, Part 3 `phrase/2-3`, and bundled-library autoloaded predicates | Strict registry contains only the Part 1 + Corrigenda core registry. |
125
126
  | 5.5.10 Evaluable functors | Normal mode additionally accepts the EyeProlog evaluable atom `e`; the remaining arithmetic functors accepted by strict mode are the Part 1 + Corrigenda set. | Strict mode rejects `e/0` as non-evaluable and retains the Corrigendum arithmetic additions. **covered** — strict extension-boundary regression plus `src/iso-arithmetic.js`. |
126
127
  | 5.5.11 Reserved atoms | None | None. |
@@ -23,7 +23,7 @@ chosen behavior is also indexed by `ISO-IMPLEMENTATION-DEFINED.md`.
23
23
  | 7.4.2.7 `include/1` | covered | The included text is prepared at the directive position and shares operator/character/flag preparation state with its parent. |
24
24
  | 7.4.2.8 `ensure_loaded/1` | covered | A source is prepared at most once in the current load graph, including repeated references and self/top-level references. |
25
25
  | 7.4.2.9 `set_prolog_flag/2` | covered | Preparation-time flag changes affect subsequent text and are replayed into execution state; strict flag names/values/changeability remain governed by the closed 7.11 audit. |
26
- | 7.4.3 source clauses | covered | Source heads/bodies are validated like program clauses, standardized static/control procedures are protected, declarations can create empty procedures, and body conversion follows 7.6.2 while preserving head/body variable identity. |
26
+ | 7.4.3 source clauses | covered | Source heads/bodies are validated like program clauses, standardized static/control procedures are protected, declarations can create empty procedures, and body conversion follows 7.6.2 while preserving head/body variable identity. The built-in restriction applies in every execution mode, so consulting a clause for a built-in predicate or control construct reports the same `permission_error(modify, static_procedure)` as `assert`ing one; EyeProlog's own library and extension predicates are not standard built-ins and stay redefinable. Corpus: `error/iso/consult_redefines_builtin`. |
27
27
 
28
28
  The strict release test `closes ISO 7.4 Prolog-text preparation and directive
29
29
  rows` exercises the cross-text operator/character/flag state, include and
@@ -37,7 +37,7 @@ and preparation error cases.
37
37
  | --- | --- | --- |
38
38
  | 7.5.1 initial database and clause order | covered | Prepared clauses retain textual preparation order. The implementation-defined initial database/standard-procedure choices are documented separately. |
39
39
  | 7.5.2 static and dynamic procedures | covered | Source procedures are static unless declared dynamic; asserting a previously absent predicate creates a dynamic procedure; modification of protected/static procedures is rejected. |
40
- | 7.5.3 private and public procedures | covered | User static procedures are private to `clause/2` in every execution mode, not only under `--iso-strict`; dynamic procedures are public; standardized built-ins/control forms remain static/private. Corpus: `iso/clause_static_and_dynamic_access`, `error/iso/clause_static_user_procedure`. |
40
+ | 7.5.3 private and public procedures | covered | User static procedures are private to `clause/2` in every execution mode, not only under `--iso-strict`; dynamic procedures are public; standardized built-ins/control forms remain static/private. The NOTE's suggested `public/1` directive is provided as an extension in normal mode, together with a `default_procedure_access` flag that declares every user-defined procedure public; both grant read access only and leave the procedures static. Corpus: `iso/clause_static_and_dynamic_access`, `iso/public_procedure_access`, `error/iso/clause_static_user_procedure`. |
41
41
  | 7.5.4 database update visibility | covered | Dynamic calls use the logical-update view: an activation continues over the clause set visible when it began, while later activations see successful assertions/retractions. `retractall/1` retains an empty dynamic procedure and `abolish/1` removes it. |
42
42
 
43
43
  The strict test `closes ISO 7.5-7.7 database, conversion, and execution rows`
@@ -140,7 +140,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
140
140
  Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
141
141
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
142
142
 
143
- The corpus has 391 cases in `iso/` and 808 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time; release/report checks separately verify the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
143
+ The corpus has 393 cases in `iso/` and 810 file-based conformance cases in total. Of those, 11 cases in `stc/` are explicitly labelled working-draft review evidence rather than normative ISO claims. The separate vendored strict-reader WG17 matrix is a deterministic regression snapshot; the live Neumerkel gate discovers the current upstream inventory at run time; release/report checks separately verify the tracked `NEUMERKEL-LATEST.md`. The generated `conformance-report.md` records local corpus totals and links to that live evidence. Together with regression, documentation-sync, API, example, and book-example checks, `npm test` is the release gate.
144
144
 
145
145
  ## Updating expected output
146
146
 
@@ -0,0 +1,52 @@
1
+ % ISO 7.5.3 notes that "an additional directive public/1 that specifies some
2
+ % user-defined procedures to be public would be an extension". EyeProlog
3
+ % provides that directive, plus a default_procedure_access flag that grants
4
+ % clause/2 access to every user-defined procedure at once, which a
5
+ % meta-interpreter needs without annotating the program it interprets.
6
+ %
7
+ % Both are read access only. A public procedure is still static, so assert/1
8
+ % and retract/1 continue to report permission_error(modify, static_procedure),
9
+ % and built-in procedures stay private either way.
10
+ % https://github.com/eyereasoner/eyeprolog/issues/96
11
+
12
+ :- public(elk/1).
13
+ elk(X) :- moose(X).
14
+
15
+ moose(bertha).
16
+
17
+ %% goal: declared_public(X0)
18
+
19
+ declared_public(Body) :-
20
+ clause(elk(bertha), Body).
21
+
22
+ %% goal: undeclared_sibling(X0)
23
+
24
+ % moose/1 carries no declaration, so it stays private.
25
+ undeclared_sibling(Culprit) :-
26
+ catch(clause(moose(_), _), error(Formal, _), true),
27
+ Formal = permission_error(access, private_procedure, Culprit).
28
+
29
+ %% goal: public_is_not_dynamic(X0)
30
+
31
+ public_is_not_dynamic(Formal) :-
32
+ catch(assertz(elk(clara)), error(Formal, _), true).
33
+
34
+ %% goal: flag_default(X0)
35
+
36
+ flag_default(Access) :-
37
+ current_prolog_flag(default_procedure_access, Access).
38
+
39
+ %% goal: flag_opens_every_procedure(X0)
40
+
41
+ flag_opens_every_procedure(Body) :-
42
+ set_prolog_flag(default_procedure_access, public),
43
+ clause(moose(bertha), Body),
44
+ set_prolog_flag(default_procedure_access, private).
45
+
46
+ %% goal: builtins_stay_private(X0)
47
+
48
+ builtins_stay_private(Culprit) :-
49
+ set_prolog_flag(default_procedure_access, public),
50
+ catch(clause(atom(_), _), error(Formal, _), true),
51
+ set_prolog_flag(default_procedure_access, private),
52
+ Formal = permission_error(access, private_procedure, Culprit).
@@ -0,0 +1,11 @@
1
+ % ISO 7.4.3: a clause of a clause-term in a Prolog text must satisfy the same
2
+ % constraints as a successful assertz/1, waiving only the static-procedure
3
+ % error, and the predicate indicator of its head must not be that of a built-in
4
+ % predicate or a control construct. Consulting a definition of clause/2 is
5
+ % therefore a permission error, exactly as asserta/1 already reported.
6
+ % https://github.com/eyereasoner/eyeprolog/issues/97
7
+ %% goal: answer
8
+
9
+ clause(elk(1), 2).
10
+
11
+ answer.
@@ -1,3 +1,3 @@
1
1
  answer(7, red, caught(eyeprolog), first, caught(eyeprolog)).
2
2
  answer(7, red, caught(eyeprolog), second, caught(eyeprolog)).
3
- flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
3
+ flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true), pair(default_procedure_access, private)]).
@@ -0,0 +1,6 @@
1
+ declared_public(moose(bertha)).
2
+ undeclared_sibling(moose / 1).
3
+ public_is_not_dynamic(permission_error(modify, static_procedure, elk / 1)).
4
+ flag_default(private).
5
+ flag_opens_every_procedure(true).
6
+ builtins_stay_private(atom / 1).
@@ -0,0 +1 @@
1
+ error(permission_error(modify, static_procedure), /(clause, 2))
@@ -4907,6 +4907,192 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
4907
4907
  assertEqual(answers.length, 2, 'cut did not prune the second clause');
4908
4908
  },
4909
4909
  },
4910
+ {
4911
+ // https://github.com/eyereasoner/eyeprolog/issues/97
4912
+ name: 'a Prolog text cannot supply clauses for a built-in predicate',
4913
+ run: () => {
4914
+ let caught = null;
4915
+ try {
4916
+ Program.parse('clause(elk(1), 2).\n');
4917
+ } catch (error) {
4918
+ caught = error;
4919
+ }
4920
+ if (!caught) throw new Error('a clause for clause/2 was accepted from a Prolog text');
4921
+ assertEqual(caught.formal, 'permission_error(modify, static_procedure)', 'formal error');
4922
+ assertEqual(termToString(caught.culprit), '/(clause, 2)', 'culprit predicate indicator');
4923
+ },
4924
+ },
4925
+ {
4926
+ name: 'consult and asserta/1 agree on redefining a built-in',
4927
+ run: () => {
4928
+ let textFormal = null;
4929
+ try {
4930
+ Program.parse('clause(elk(1), 2).\n');
4931
+ } catch (error) {
4932
+ textFormal = error.formal;
4933
+ }
4934
+ const solver = new Solver(Program.parse('run :- asserta(clause(elk(1), 2)).\n'), {});
4935
+ let assertFormal = null;
4936
+ try {
4937
+ [...solver.solve([parseGoalText('run')], new Env(), 0)];
4938
+ } catch (error) {
4939
+ assertFormal = error.formal;
4940
+ }
4941
+ assertEqual(textFormal, 'permission_error(modify, static_procedure)', 'consult path');
4942
+ assertEqual(assertFormal, textFormal, 'assert path agrees with consult');
4943
+ },
4944
+ },
4945
+ {
4946
+ // Control constructs are covered as well as built-in predicates.
4947
+ name: 'a Prolog text cannot supply clauses for a control construct',
4948
+ run: () => {
4949
+ for (const [source, indicator] of [
4950
+ ["';'(a, b).\n", "/(';', 2)"],
4951
+ ["','(a, b).\n", "/(',', 2)"],
4952
+ ['!.\n', '/(!, 0)'],
4953
+ ]) {
4954
+ let caught = null;
4955
+ try {
4956
+ Program.parse(source);
4957
+ } catch (error) {
4958
+ caught = error;
4959
+ }
4960
+ assertEqual(caught?.formal, 'permission_error(modify, static_procedure)', `formal error for ${indicator}`);
4961
+ assertEqual(termToString(caught.culprit), indicator, `culprit for ${indicator}`);
4962
+ }
4963
+ },
4964
+ },
4965
+ {
4966
+ // EyeProlog's library and extension predicates are not standard
4967
+ // built-ins, so user programs may still define their own versions.
4968
+ name: 'a Prolog text may still redefine library predicates',
4969
+ run: () => {
4970
+ const program = Program.parse('member(X, [X]).\nappend([], L, L).\n');
4971
+ assertEqual(program.findGroup('member', 2) != null, true, 'member/2 defined by the text');
4972
+ assertEqual(program.findGroup('append', 3) != null, true, 'append/3 defined by the text');
4973
+ },
4974
+ },
4975
+ {
4976
+ // A dynamic/1 directive names a procedure the text is about to define,
4977
+ // so it is subject to the same restriction.
4978
+ name: 'a dynamic/1 directive cannot name a built-in predicate',
4979
+ run: () => {
4980
+ let caught = null;
4981
+ try {
4982
+ Program.parse(':- dynamic(atom_length/2).\n');
4983
+ } catch (error) {
4984
+ caught = error;
4985
+ }
4986
+ assertEqual(caught?.formal, 'permission_error(modify, static_procedure)', 'formal error');
4987
+ assertEqual(termToString(caught.culprit), '/(atom_length, 2)', 'culprit predicate indicator');
4988
+ },
4989
+ },
4990
+ {
4991
+ // ISO 7.5.3 NOTE: public/1 as an extension.
4992
+ name: 'a public/1 directive grants clause/2 access to a static procedure',
4993
+ run: () => {
4994
+ const program = Program.parse(':- public(elk/1).\nelk(X) :- moose(X).\nmoose(bertha).\n');
4995
+ const solver = new Solver(program, {});
4996
+ const answers = [...solver.solve([parseGoalText('clause(elk(_), _)')], new Env(), 0)];
4997
+ assertEqual(answers.length, 1, 'declared public procedure is readable');
4998
+ assertEqual(program.findGroup('elk', 1).dynamic, false, 'public does not imply dynamic');
4999
+ },
5000
+ },
5001
+ {
5002
+ name: 'a public/1 declaration still refuses database modification',
5003
+ run: () => {
5004
+ const solver = new Solver(Program.parse(':- public(elk/1).\nelk(bertha).\n'), {});
5005
+ for (const goal of ['assertz(elk(clara))', 'retract(elk(bertha))']) {
5006
+ let caught = null;
5007
+ try {
5008
+ [...solver.solve([parseGoalText(goal)], new Env(), 0)];
5009
+ } catch (error) {
5010
+ caught = error;
5011
+ }
5012
+ assertEqual(caught?.formal, 'permission_error(modify, static_procedure)', `${goal} is refused`);
5013
+ }
5014
+ },
5015
+ },
5016
+ {
5017
+ name: 'public/1 applies to clauses that precede the directive',
5018
+ run: () => {
5019
+ const program = Program.parse('elk(bertha).\n:- public(elk/1).\n');
5020
+ const solver = new Solver(program, {});
5021
+ const answers = [...solver.solve([parseGoalText('clause(elk(_), _)')], new Env(), 0)];
5022
+ assertEqual(answers.length, 1, 'earlier clauses become readable');
5023
+ },
5024
+ },
5025
+ {
5026
+ name: 'public/1 leaves undeclared procedures private',
5027
+ run: () => {
5028
+ const solver = new Solver(Program.parse(':- public(elk/1).\nelk(X) :- moose(X).\nmoose(bertha).\n'), {});
5029
+ let caught = null;
5030
+ try {
5031
+ [...solver.solve([parseGoalText('clause(moose(_), _)')], new Env(), 0)];
5032
+ } catch (error) {
5033
+ caught = error;
5034
+ }
5035
+ assertEqual(caught?.formal, 'permission_error(access, private_procedure)', 'sibling stays private');
5036
+ },
5037
+ },
5038
+ {
5039
+ // The meta-interpreter case: no annotation on the interpreted program.
5040
+ name: 'default_procedure_access public opens every user-defined procedure',
5041
+ run: () => {
5042
+ const source = ':- set_prolog_flag(default_procedure_access, public).\n' +
5043
+ 'solve(true) :- !.\n' +
5044
+ 'solve((A, B)) :- !, solve(A), solve(B).\n' +
5045
+ 'solve(H) :- clause(H, Body), solve(Body).\n' +
5046
+ 'elk(X) :- moose(X).\nmoose(bertha).\ngrazes(X) :- elk(X).\n';
5047
+ const solver = new Solver(Program.parse(source), {});
5048
+ const goal = parseGoalText('solve(grazes(W))');
5049
+ const answers = [...solver.solve([goal], new Env(), 0)];
5050
+ assertEqual(answers.length, 1, 'meta-interpreter answer count');
5051
+ assertEqual(termToString(copyResolved(goal.args[0], answers[0])), 'grazes(bertha)', 'meta-interpreter answer');
5052
+ },
5053
+ },
5054
+ {
5055
+ name: 'default_procedure_access public keeps procedures static and built-ins private',
5056
+ run: () => {
5057
+ const source = ':- set_prolog_flag(default_procedure_access, public).\nmoose(bertha).\n';
5058
+ const solver = new Solver(Program.parse(source), {});
5059
+ for (const [goal, formal] of [
5060
+ ['retract(moose(bertha))', 'permission_error(modify, static_procedure)'],
5061
+ ['clause(atom(_), _)', 'permission_error(access, private_procedure)'],
5062
+ ]) {
5063
+ let caught = null;
5064
+ try {
5065
+ [...solver.solve([parseGoalText(goal)], new Env(), 0)];
5066
+ } catch (error) {
5067
+ caught = error;
5068
+ }
5069
+ assertEqual(caught?.formal, formal, `${goal} is refused`);
5070
+ }
5071
+ },
5072
+ },
5073
+ {
5074
+ // public/1 and the flag are extensions, so the strict profile omits both.
5075
+ name: 'strict ISO core mode offers neither public/1 nor the access flag',
5076
+ run: () => {
5077
+ let caught = null;
5078
+ try {
5079
+ Program.parse(':- public(elk/1).\nelk(bertha).\n', { isoStrict: true });
5080
+ } catch (error) {
5081
+ caught = error;
5082
+ }
5083
+ if (!caught) throw new Error('public/1 was accepted in strict ISO core mode');
5084
+ // 8.17.4.3: an unknown flag name is a domain error, so the flag being
5085
+ // absent from the strict profile is observable that way.
5086
+ const solver = new Solver(Program.parse('elk(bertha).\n', { isoStrict: true }), { isoStrict: true });
5087
+ let flagError = null;
5088
+ try {
5089
+ [...solver.solve([parseGoalText('current_prolog_flag(default_procedure_access, _)', { isoStrict: true })], new Env(), 0)];
5090
+ } catch (error) {
5091
+ flagError = error;
5092
+ }
5093
+ assertEqual(flagError?.formal, 'domain_error(prolog_flag)', 'flag is absent in strict ISO core mode');
5094
+ },
5095
+ },
4910
5096
  {
4911
5097
  // https://github.com/eyereasoner/eyeprolog/issues/96
4912
5098
  name: 'clause/2 keeps static procedures private in the default mode',
@@ -8170,14 +8356,16 @@ function whiteBoxCases() {
8170
8356
  assertEqual(termToString(candidates.primary[0].head, new Env(), true), 'row(a0, b0, c0, first)', 'first head');
8171
8357
  assertEqual(termToString(candidates.primary[1].head, new Env(), true), 'row(a0, X, c0, wildcard)', 'wildcard head');
8172
8358
 
8359
+ // A user-defined name: ISO 7.4.3 forbids a Prolog text from defining
8360
+ // clauses for a built-in predicate such as open/3.
8173
8361
  const variableHeavy = Program.parse(Array.from(
8174
8362
  { length: 12 },
8175
- (_, index) => `open(X${index}, Y${index}, value${index}).`,
8363
+ (_, index) => `gate(X${index}, Y${index}, value${index}).`,
8176
8364
  ).join('\n'));
8177
- const openGroup = variableHeavy.findGroup('open', 3);
8178
- selectClauseCandidates(openGroup, parseGoalText('open(a, b, Result)'), new Env());
8179
- assertEqual(openGroup.demandIndexes.size, 0, 'poor wide index discarded');
8180
- assertEqual(openGroup.rejectedDemandIndexes.has('0,1'), true, 'poor call mode remembered');
8365
+ const gateGroup = variableHeavy.findGroup('gate', 3);
8366
+ selectClauseCandidates(gateGroup, parseGoalText('gate(a, b, Result)'), new Env());
8367
+ assertEqual(gateGroup.demandIndexes.size, 0, 'poor wide index discarded');
8368
+ assertEqual(gateGroup.rejectedDemandIndexes.has('0,1'), true, 'poor call mode remembered');
8181
8369
  },
8182
8370
  },
8183
8371
  {
@@ -770,13 +770,18 @@ Equality in the pure Herbrand reading is syntactic identity after substitution.
770
770
  Operationally, unification discovers a substitution that makes terms
771
771
  identical. EyeProlog performs an occurs check whenever unification would bind a
772
772
  variable. It therefore uses finite-tree unification and rejects a binding when
773
- the variable occurs anywhere in the proposed value. For example, this call
774
- fails rather than constructing a cyclic term:
773
+ the variable occurs anywhere in the proposed value. For example, the body of this
774
+ clause fails rather than constructing a cyclic term:
775
775
 
776
776
  ```eyeprolog
777
- (X = wrapper(X)).
777
+ cyclic_unification :- X = wrapper(X).
778
778
  ```
779
779
 
780
+ The goal has to be written inside a clause of its own: ISO 7.4.3 forbids a
781
+ Prolog text from supplying clauses for a built-in predicate, so a bare
782
+ `(X = wrapper(X)).` in a program file is a clause for `(=)/2` and is rejected
783
+ with *permission_error(modify, static_procedure, (=)/2)*.
784
+
780
785
  ISO classifies unifications whose outcome depends on an occurs check as
781
786
  subject-to-occurs-check (STO). EyeProlog's default remains the sound finite-tree
782
787
  behavior above. For diagnosis, EyeProlog additionally provides the
@@ -6146,7 +6151,7 @@ partial list.
6146
6151
 
6147
6152
  #### Dynamic database and procedure information
6148
6153
 
6149
- - **`clause(+Head,?Body)`** — Enumerates fresh copies of source clauses matching the callable `Head`; facts have body `true`. Only *public* procedures can be inspected: a procedure defined by a Prolog text is static unless a *dynamic/1* directive declares it, and a procedure first created by *assertz/1* or *asserta/1* is dynamic. Access to a static user procedure, or to a built-in, raises *permission_error(access,private_procedure)*. This applies in every execution mode, not only under `--iso-strict`, and matches the *permission_error(modify,static_procedure)* that *assertz/1* and *retract/1* already raise for the same procedures.
6154
+ - **`clause(+Head,?Body)`** — Enumerates fresh copies of source clauses matching the callable `Head`; facts have body `true`. Only *public* procedures can be inspected: a procedure defined by a Prolog text is static unless a *dynamic/1* directive declares it, and a procedure first created by *assertz/1* or *asserta/1* is dynamic. Access to a static user procedure, or to a built-in, raises *permission_error(access,private_procedure)*. This applies in every execution mode, not only under `--iso-strict`, and matches the *permission_error(modify,static_procedure)* that *assertz/1* and *retract/1* already raise for the same procedures. Normal mode additionally accepts a *public/1* directive and a `default_procedure_access` flag that open static procedures to inspection; see below.
6150
6155
  - **`asserta(+Clause)`, `assertz(+Clause)`** — Insert a copied fact or rule at the beginning or end of a predicate declared *dynamic/1*. Static and built-in procedures cannot be modified.
6151
6156
  - **`retract(+Clause)`** — Removes matching dynamic clauses one at a time on backtracking. A call sees the logical update view captured when it began. A fact pattern matches facts only.
6152
6157
  - **`retractall(+Head)`** — Removes every matching clause from a dynamic procedure, succeeds when none match, and keeps the empty dynamic procedure known.
@@ -6200,6 +6205,58 @@ consults another file or imports a module instead of being reset by the host
6200
6205
  rebuild of the program. Programs that intentionally treat an undefined
6201
6206
  predicate as failure must opt in with `set_prolog_flag(unknown, fail)`; bundled
6202
6207
  examples and non-ISO corpus cases that depend on that policy do so explicitly.
6208
+ ### Reading static procedures
6209
+
6210
+ A procedure defined by a Prolog text is static, so `clause/2` refuses it
6211
+ (ISO 7.5.2, 7.5.3, 8.8.1.3). Declaring the procedure `dynamic` lifts the
6212
+ restriction, but it also makes the procedure modifiable, and for a
6213
+ meta-interpreter it means annotating a program you may not want to edit and
6214
+ enumerating every predicate you intend to read.
6215
+
6216
+ ISO 7.5.3 has a NOTE observing that a `public/1` directive declaring
6217
+ user-defined procedures to be public would be an extension. Normal EyeProlog
6218
+ provides it:
6219
+
6220
+ ```eyeprolog
6221
+ :- public(elk/1).
6222
+
6223
+ elk(X) :- moose(X).
6224
+
6225
+ moose(bertha).
6226
+ ```
6227
+
6228
+ `clause(elk(bertha), Body)` now succeeds with `Body = moose(bertha)`, while
6229
+ `moose/1` carries no declaration and stays private. A public procedure is still
6230
+ *static*: `assertz(elk(clara))` continues to raise
6231
+ *permission_error(modify,static_procedure)*. The directive grants read access
6232
+ only.
6233
+
6234
+ To open every user-defined procedure at once, set the
6235
+ `default_procedure_access` flag to `public`:
6236
+
6237
+ ```eyeprolog
6238
+ :- set_prolog_flag(default_procedure_access, public).
6239
+
6240
+ solve(true) :- !.
6241
+ solve((A, B)) :- !, solve(A), solve(B).
6242
+ solve(H) :- clause(H, Body), solve(Body).
6243
+
6244
+ elk(X) :- moose(X).
6245
+ moose(bertha).
6246
+ grazes(X) :- elk(X).
6247
+ ```
6248
+
6249
+ `solve(grazes(W))` yields `W = bertha` without a single declaration on the
6250
+ interpreted program. The flag changes access, not mutability or existence:
6251
+ procedures remain static, and built-in procedures remain private, so
6252
+ `clause(atom(_), _)` still raises *permission_error(access,private_procedure)*.
6253
+ The supported values are `private` (the default, matching ISO) and `public`.
6254
+
6255
+ Both the directive and the flag are extensions, so strict ISO core mode offers
6256
+ neither: `public/1` is rejected as an implementation-specific directive, and
6257
+ `current_prolog_flag(default_procedure_access, _)` raises
6258
+ *domain_error(prolog_flag)*.
6259
+
6203
6260
  The `occurs_check` flag is an EyeProlog diagnostic
6204
6261
  extension rather than an ISO-defined core flag: it is absent in strict mode,
6205
6262
  while normal mode keeps its `true` default and optional `error` diagnostic for
@@ -10365,7 +10422,7 @@ must preserve the same observable outcome. Additional normal-mode syntax may
10365
10422
  accept texts outside the strict grammar, but it may not reinterpret an accepted
10366
10423
  standard case.
10367
10424
 
10368
- The file-based conformance corpus contains 808 cases, including 391 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
10425
+ The file-based conformance corpus contains 810 cases, including 393 focused ISO cases derived from the success, failure, mode, and error behavior in ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
10369
10426
  Separate exact-output suites check 210 normal examples and 61 proof examples; all executable chapter programs are parsed and their declared goals are executed. The eight-case
10370
10427
  playground contract suite imports the production worker, sends real reasoning
10371
10428
  requests through its message protocol, and crawls the served module graph for
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
+ import { Program, getStrictIsoRegistry } from '../src/index.js';
8
+
7
9
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
10
  const bookPath = path.join(root, 'the-art-of-eyeprolog.md');
9
11
  const outputRoot = path.join(root, 'examples', 'book');
@@ -36,6 +38,11 @@ for (const match of book.matchAll(/^(## \d+\. ([^\n]+)|# ([^\n]+)|```eyeprolog\n
36
38
  if (!chapter) continue;
37
39
  const source = match[4].trim();
38
40
  if (!source.endsWith('.')) continue;
41
+ // The book also uses eyeprolog blocks for template listings and single
42
+ // illustrative goals. Those read as clauses for a built-in predicate, which
43
+ // ISO 7.4.3 forbids a Prolog text from defining, so they are not programs
44
+ // and are not extracted as such.
45
+ if (definesBuiltIn(source)) continue;
39
46
 
40
47
  const firstClause = source
41
48
  .split('\n')
@@ -46,6 +53,27 @@ for (const match of book.matchAll(/^(## \d+\. ([^\n]+)|# ([^\n]+)|```eyeprolog\n
46
53
  chapter.examples.push({ predicate, section, source });
47
54
  }
48
55
 
56
+ // True when a code block supplies clauses for a standard built-in predicate or
57
+ // control construct, which marks it as a signature listing or goal fragment
58
+ // rather than an extractable program.
59
+ function definesBuiltIn(source) {
60
+ try {
61
+ const program = Program.parse(source);
62
+ const registry = getStrictIsoRegistry();
63
+ for (const group of program.groups.values()) {
64
+ if (group.name === ',' && group.arity === 2) return true;
65
+ if (registry.get(group.name, group.arity)) return true;
66
+ }
67
+ return false;
68
+ } catch (error) {
69
+ // Preparation itself rejects a clause head that is a built-in, so that
70
+ // error is the signal we are looking for. Any other parse failure is left
71
+ // to the book-example test, which reports it instead of silently dropping
72
+ // the block here.
73
+ return error?.formal === 'permission_error(modify, static_procedure)';
74
+ }
75
+ }
76
+
49
77
  const files = new Map();
50
78
  const readme = [
51
79
  '# The Art of EyeProlog — inline examples',
@@ -1,8 +0,0 @@
1
- % From The Art of EyeProlog, Chapter 8.
2
- :- use_module(library(aggregate)).
3
- :- use_module(library(lists)).
4
- :- use_module(library(iso_ext)).
5
-
6
- findall(Template, Goal, List).
7
- countall(Goal, Count).
8
- sumall(Value, Goal, Sum).
@@ -1,4 +0,0 @@
1
- % From The Art of EyeProlog, Chapter 9.
2
- functor(Term, Name, Arity).
3
- arg(Index, Term, Value).
4
- (Term =.. [Name | Arguments]).
@@ -1,6 +0,0 @@
1
- % From The Art of EyeProlog, Chapter 24 — Existence, one witness, and all witnesses.
2
- :- use_module(library(lists)).
3
-
4
- reachable(From, To).
5
- once(simple_path(From, To, Path)).
6
- findall(Path, simple_path(From, To, Path), Paths).
@@ -1,2 +0,0 @@
1
- % From The Art of EyeProlog, Chapter 28 — Unification is structural equation solving.
2
- (pair(X, f(Y)) = pair(g(a), f(b))).