eyeprolog 1.5.50 → 1.5.51

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 (25) hide show
  1. package/conformance-report.md +2 -2
  2. package/examples/book/README.md +9 -13
  3. package/examples/book/chapter-03/09-program.pl +1 -1
  4. package/package.json +1 -1
  5. package/src/program.js +21 -9
  6. package/test/conformance/ISO-PROLOG-TEXT-EXECUTION-MATRIX.md +1 -1
  7. package/test/conformance/README.md +1 -1
  8. package/test/conformance/errors/iso/consult_redefines_builtin.pl +11 -0
  9. package/test/conformance/expected-errors/iso/consult_redefines_builtin.txt +1 -0
  10. package/test/run-regression.mjs +87 -5
  11. package/the-art-of-eyeprolog.md +9 -4
  12. package/tools/extract-book-examples.mjs +28 -0
  13. package/examples/book/chapter-08/01-findall.pl +0 -8
  14. package/examples/book/chapter-09/01-functor.pl +0 -4
  15. package/examples/book/chapter-24/03-reachable.pl +0 -6
  16. package/examples/book/chapter-28/01-program.pl +0 -2
  17. /package/examples/book/chapter-08/{02-outgoing_costs.pl → 01-outgoing_costs.pl} +0 -0
  18. /package/examples/book/chapter-08/{03-best_route.pl → 02-best_route.pl} +0 -0
  19. /package/examples/book/chapter-09/{02-normalized.pl → 01-normalized.pl} +0 -0
  20. /package/examples/book/chapter-09/{03-message.pl → 02-message.pl} +0 -0
  21. /package/examples/book/chapter-24/{04-best_plan.pl → 03-best_plan.pl} +0 -0
  22. /package/examples/book/chapter-28/{02-triangle.pl → 01-triangle.pl} +0 -0
  23. /package/examples/book/chapter-28/{03-rectangle.pl → 02-rectangle.pl} +0 -0
  24. /package/examples/book/chapter-28/{04-integer_rectangle.pl → 03-integer_rectangle.pl} +0 -0
  25. /package/examples/book/chapter-28/{05-preserves_combine.pl → 04-preserves_combine.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 | 173 | 219 | 0 | 0 | 392 |
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** | **495** | **274** | **19** | **21** | **809** |
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
 
@@ -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).
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.51",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/program.js CHANGED
@@ -1879,16 +1879,28 @@ function assertDynamicIndicatorIsDefinable(indicator, strictIso = false) {
1879
1879
 
1880
1880
  function assertPredicateIsDefinable(name, arity, strictIso = false) {
1881
1881
  // 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
- );
1882
+ // be redefined in either profile.
1883
+ //
1884
+ // ISO 7.4.3 requires that the predicate indicator of the head of a clause in
1885
+ // a Prolog text is not that of a built-in predicate or a control construct.
1886
+ // That subclause otherwise defers to assertz/1 and waives only the static
1887
+ // procedure error, so the built-in restriction is a separate requirement and
1888
+ // is not profile-dependent. The strict ISO registry holds exactly the Part 1
1889
+ // built-ins and control constructs, so it is consulted in every mode.
1890
+ //
1891
+ // EyeProlog's library and extension predicates are deliberately absent from
1892
+ // that registry and stay redefinable, which preserves the source
1893
+ // compatibility that user programs rely on.
1894
+ //
1895
+ // (,)/2 is a control construct (table 9) but is recognized by the parser
1896
+ // rather than the registry, so it is named here. (:-)/1-2 come from the STC
1897
+ // #56 working draft rather than published text and stay strict-only.
1898
+ const controlConstruct = name === ',' && arity === 2;
1899
+ const draftSyntaxProcedure = strictIso && name === ':-' && (arity === 1 || arity === 2);
1889
1900
  if ((name === 'false' && arity === 0) ||
1890
- (strictIso && getStrictIsoRegistry().get(name, arity)) ||
1891
- strictSyntaxProcedure) {
1901
+ getStrictIsoRegistry().get(name, arity) ||
1902
+ controlConstruct ||
1903
+ draftSyntaxProcedure) {
1892
1904
  throw staticProcedureModificationError(name, arity);
1893
1905
  }
1894
1906
  }
@@ -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
@@ -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 392 cases in `iso/` and 809 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,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.
@@ -0,0 +1 @@
1
+ error(permission_error(modify, static_procedure), /(clause, 2))
@@ -4907,6 +4907,86 @@ 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
+ },
4910
4990
  {
4911
4991
  // https://github.com/eyereasoner/eyeprolog/issues/96
4912
4992
  name: 'clause/2 keeps static procedures private in the default mode',
@@ -8170,14 +8250,16 @@ function whiteBoxCases() {
8170
8250
  assertEqual(termToString(candidates.primary[0].head, new Env(), true), 'row(a0, b0, c0, first)', 'first head');
8171
8251
  assertEqual(termToString(candidates.primary[1].head, new Env(), true), 'row(a0, X, c0, wildcard)', 'wildcard head');
8172
8252
 
8253
+ // A user-defined name: ISO 7.4.3 forbids a Prolog text from defining
8254
+ // clauses for a built-in predicate such as open/3.
8173
8255
  const variableHeavy = Program.parse(Array.from(
8174
8256
  { length: 12 },
8175
- (_, index) => `open(X${index}, Y${index}, value${index}).`,
8257
+ (_, index) => `gate(X${index}, Y${index}, value${index}).`,
8176
8258
  ).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');
8259
+ const gateGroup = variableHeavy.findGroup('gate', 3);
8260
+ selectClauseCandidates(gateGroup, parseGoalText('gate(a, b, Result)'), new Env());
8261
+ assertEqual(gateGroup.demandIndexes.size, 0, 'poor wide index discarded');
8262
+ assertEqual(gateGroup.rejectedDemandIndexes.has('0,1'), true, 'poor call mode remembered');
8181
8263
  },
8182
8264
  },
8183
8265
  {
@@ -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
@@ -10365,7 +10370,7 @@ must preserve the same observable outcome. Additional normal-mode syntax may
10365
10370
  accept texts outside the strict grammar, but it may not reinterpret an accepted
10366
10371
  standard case.
10367
10372
 
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.
10373
+ The file-based conformance corpus contains 809 cases, including 392 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
10374
  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
10375
  playground contract suite imports the production worker, sends real reasoning
10371
10376
  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))).