eyeprolog 1.5.49 → 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.
- package/conformance-report.md +2 -2
- package/examples/book/README.md +9 -13
- package/examples/book/chapter-03/09-program.pl +1 -1
- package/examples/iso-grouped-solutions.pl +3 -0
- package/package.json +1 -1
- package/src/iso.js +8 -2
- package/src/program.js +29 -10
- package/test/conformance/ISO-BUILTIN-MODE-ERROR-MATRIX.md +1 -1
- package/test/conformance/ISO-PROLOG-TEXT-EXECUTION-MATRIX.md +3 -3
- package/test/conformance/README.md +1 -1
- package/test/conformance/cases/iso/clause_body_conversion.pl +41 -0
- package/test/conformance/cases/iso/clause_static_and_dynamic_access.pl +26 -0
- package/test/conformance/cases/iso/grouped_solutions_and_clauses.pl +5 -0
- package/test/conformance/cases/stc/clause_variable_identity.pl +2 -0
- package/test/conformance/errors/iso/clause_static_user_procedure.pl +13 -0
- package/test/conformance/errors/iso/consult_redefines_builtin.pl +11 -0
- package/test/conformance/expected/iso/clause_body_conversion.pl +4 -0
- package/test/conformance/expected/iso/clause_static_and_dynamic_access.pl +3 -0
- package/test/conformance/expected-errors/iso/clause_static_user_procedure.txt +1 -0
- package/test/conformance/expected-errors/iso/consult_redefines_builtin.txt +1 -0
- package/test/run-regression.mjs +213 -5
- package/the-art-of-eyeprolog.md +10 -5
- package/tools/extract-book-examples.mjs +28 -0
- package/examples/book/chapter-08/01-findall.pl +0 -8
- package/examples/book/chapter-09/01-functor.pl +0 -4
- package/examples/book/chapter-24/03-reachable.pl +0 -6
- package/examples/book/chapter-28/01-program.pl +0 -2
- /package/examples/book/chapter-08/{02-outgoing_costs.pl → 01-outgoing_costs.pl} +0 -0
- /package/examples/book/chapter-08/{03-best_route.pl → 02-best_route.pl} +0 -0
- /package/examples/book/chapter-09/{02-normalized.pl → 01-normalized.pl} +0 -0
- /package/examples/book/chapter-09/{03-message.pl → 02-message.pl} +0 -0
- /package/examples/book/chapter-24/{04-best_plan.pl → 03-best_plan.pl} +0 -0
- /package/examples/book/chapter-28/{02-triangle.pl → 01-triangle.pl} +0 -0
- /package/examples/book/chapter-28/{03-rectangle.pl → 02-rectangle.pl} +0 -0
- /package/examples/book/chapter-28/{04-integer_rectangle.pl → 03-integer_rectangle.pl} +0 -0
- /package/examples/book/chapter-28/{05-preserves_combine.pl → 04-preserves_combine.pl} +0 -0
package/conformance-report.md
CHANGED
|
@@ -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 |
|
|
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** | **
|
|
48
|
+
| **Total** | **495** | **274** | **19** | **21** | **809** |
|
|
49
49
|
|
|
50
50
|
## DCG conformance clarification
|
|
51
51
|
|
package/examples/book/README.md
CHANGED
|
@@ -61,15 +61,13 @@ npm run generate
|
|
|
61
61
|
|
|
62
62
|
## Chapter 8: Collecting and choosing answers
|
|
63
63
|
|
|
64
|
-
- [01-
|
|
65
|
-
- [02-
|
|
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-
|
|
71
|
-
- [02-
|
|
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-
|
|
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-
|
|
178
|
-
- [02-
|
|
179
|
-
- [03-
|
|
180
|
-
- [04-
|
|
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
|
-
|
|
2
|
+
cyclic_unification :- X = wrapper(X).
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -733,8 +733,14 @@ function* clauseSolutions({ solver, goal, env }, state) {
|
|
|
733
733
|
// EyeProlog selects private-procedure access before Body callability when both
|
|
734
734
|
// conditions hold. ISO 7.12 permits an implementation-dependent choice when
|
|
735
735
|
// more than one error condition is simultaneously satisfied.
|
|
736
|
-
|
|
737
|
-
|
|
736
|
+
//
|
|
737
|
+
// A procedure defined by a Prolog text is static unless a dynamic/1
|
|
738
|
+
// directive says otherwise (ISO 7.5.2 and 8.9), and clause/2 may only
|
|
739
|
+
// inspect a public (dynamic) procedure (ISO 8.8.1.3). Static is therefore
|
|
740
|
+
// the default in every mode, not only under --iso-strict: assert/1,
|
|
741
|
+
// retract/1, and abolish/1 already reject static procedures unconditionally,
|
|
742
|
+
// and clause/2 now agrees with them.
|
|
743
|
+
if (isProcessorStaticProcedure(solver, head) || (group && !group.dynamic)) {
|
|
738
744
|
throw new PrologError('permission_error(access, private_procedure)', indicator);
|
|
739
745
|
}
|
|
740
746
|
callableOrVariable(goal.args[1], env);
|
package/src/program.js
CHANGED
|
@@ -778,12 +778,19 @@ class ProgramBuilder {
|
|
|
778
778
|
}
|
|
779
779
|
|
|
780
780
|
if (!isDirectiveClause(clause)) {
|
|
781
|
-
if (
|
|
781
|
+
if (Array.isArray(clause.body) && clause.body.length > 0) {
|
|
782
782
|
// ISO 7.6.2 applies when source text is prepared as well as when a
|
|
783
783
|
// clause term is asserted dynamically. Keep the original variable
|
|
784
784
|
// objects while wrapping variable goals in call/1 so sharing with
|
|
785
785
|
// the head (and recursively through ;/2 and ->/2) is preserved for
|
|
786
786
|
// later clause/2 and retract/1 conversion back to terms.
|
|
787
|
+
//
|
|
788
|
+
// 7.5.1 prepares the read-terms of a Prolog text into database
|
|
789
|
+
// clauses by this same conversion, so it is not conditional on the
|
|
790
|
+
// strict profile. Skipping it left a bare variable goal where the
|
|
791
|
+
// standard requires call/1, which both reports the wrong body term
|
|
792
|
+
// through clause/2 (8.8.1.4, legs/2) and makes a cut in that goal
|
|
793
|
+
// transparent instead of local to the call (7.8.3).
|
|
787
794
|
clause.body = clause.body.map((goal) => convertClauseBodyTerm(goal));
|
|
788
795
|
}
|
|
789
796
|
assertHeadIsDefinable(clause.head, program.strictIso);
|
|
@@ -1872,16 +1879,28 @@ function assertDynamicIndicatorIsDefinable(indicator, strictIso = false) {
|
|
|
1872
1879
|
|
|
1873
1880
|
function assertPredicateIsDefinable(name, arity, strictIso = false) {
|
|
1874
1881
|
// false/0 is standardized as a static built-in by Corrigendum 2 and cannot
|
|
1875
|
-
// be redefined in either profile.
|
|
1876
|
-
//
|
|
1877
|
-
//
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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);
|
|
1882
1900
|
if ((name === 'false' && arity === 0) ||
|
|
1883
|
-
|
|
1884
|
-
|
|
1901
|
+
getStrictIsoRegistry().get(name, arity) ||
|
|
1902
|
+
controlConstruct ||
|
|
1903
|
+
draftSyntaxProcedure) {
|
|
1885
1904
|
throw staticProcedureModificationError(name, arity);
|
|
1886
1905
|
}
|
|
1887
1906
|
}
|
|
@@ -141,7 +141,7 @@ The Corrigendum 2 additions have the same shape.
|
|
|
141
141
|
|
|
142
142
|
| Clause / predicate | Prescribed row | Status | Executable evidence |
|
|
143
143
|
| --- | --- | --- | --- |
|
|
144
|
-
| 8.8.1 `clause/2` | enumerate clauses of a public user-defined procedure | covered | dynamic-clause tests and grouped-solutions/clause corpus |
|
|
144
|
+
| 8.8.1 `clause/2` | enumerate clauses of a public user-defined procedure | covered | dynamic-clause tests and grouped-solutions/clause corpus; `iso/clause_static_and_dynamic_access` and `error/iso/clause_static_user_procedure` pin the static/private default in all modes |
|
|
145
145
|
| 8.8.1 error (a) | variable head -> instantiation error | covered | strict `clause(X,_)` |
|
|
146
146
|
| 8.8.1 error (b) | non-callable head -> callable type error | covered | strict `clause(4,_)` |
|
|
147
147
|
| 8.8.1 error (c) | private procedure -> access/private-procedure permission error | covered | strict static/private tests |
|
|
@@ -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`; dynamic procedures are public; standardized built-ins/control forms remain static/private. |
|
|
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`. |
|
|
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`
|
|
@@ -51,7 +51,7 @@ The complete 8.8-8.9 database built-in modes/errors remain covered by
|
|
|
51
51
|
| Row | Status | EyeProlog disposition / executable evidence |
|
|
52
52
|
| --- | --- | --- |
|
|
53
53
|
| 7.6.1 term-to-goal/body boundary | covered | Callable terms enter execution as goals; variables are deferred through `call/1`; non-callable numeric/body terms are rejected rather than silently coerced. |
|
|
54
|
-
| 7.6.2 term-to-body conversion | covered | Variables become `call/1`; conjunction, disjunction, and if-then bodies are converted recursively; non-control callable terms retain their predication; variable identity is preserved across the converted clause. |
|
|
54
|
+
| 7.6.2 term-to-body conversion | covered | Variables become `call/1`; conjunction, disjunction, and if-then bodies are converted recursively; non-control callable terms retain their predication; variable identity is preserved across the converted clause. The conversion runs in every execution mode, for clauses prepared from a Prolog text as well as for `assert`ed clauses, so both paths yield the same body term. Corpus: `iso/clause_body_conversion`. |
|
|
55
55
|
| 7.6.3 clause-to-term observation | covered | `clause/2` returns fresh renamed clause observations with the required sharing between head and body. |
|
|
56
56
|
| 7.6.4 update conversion | covered | `asserta/1`, `assertz/1`, `retract/1`, and prepared source clauses use the same strict body-conversion rules and reject malformed bodies before database mutation. |
|
|
57
57
|
|
|
@@ -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
|
|
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,41 @@
|
|
|
1
|
+
% ISO 7.6.2 a: when a term is converted to the body of a clause, a variable
|
|
2
|
+
% goal becomes call/1. ISO 7.6.2 b applies the conversion recursively through
|
|
3
|
+
% (,)/2, (;)/2 and (->)/2. ISO 7.5.1 prepares the read-terms of a Prolog text
|
|
4
|
+
% into database clauses by that same conversion, so a clause loaded from a
|
|
5
|
+
% Prolog text and the identical clause added by assertz/1 must agree.
|
|
6
|
+
|
|
7
|
+
:- dynamic(from_text/1).
|
|
8
|
+
from_text(Goal) :- Goal.
|
|
9
|
+
|
|
10
|
+
:- dynamic(nested/3).
|
|
11
|
+
nested(A, B, C) :- (A ; B), (C -> true ; true).
|
|
12
|
+
|
|
13
|
+
:- dynamic(cut_scope/1).
|
|
14
|
+
cut_scope(Goal) :- Goal.
|
|
15
|
+
cut_scope(_) :- true.
|
|
16
|
+
|
|
17
|
+
%% goal: text_body(X0)
|
|
18
|
+
|
|
19
|
+
text_body(Body) :-
|
|
20
|
+
clause(from_text(true), Body).
|
|
21
|
+
|
|
22
|
+
%% goal: nested_body(X0)
|
|
23
|
+
|
|
24
|
+
nested_body(Body) :-
|
|
25
|
+
clause(nested(a, b, c), Body).
|
|
26
|
+
|
|
27
|
+
%% goal: assert_agrees_with_text(X0)
|
|
28
|
+
|
|
29
|
+
% The same clause added at run time must convert to the same body term.
|
|
30
|
+
assert_agrees_with_text(Agree) :-
|
|
31
|
+
assertz((asserted(Goal) :- Goal)),
|
|
32
|
+
clause(from_text(true), TextBody),
|
|
33
|
+
clause(asserted(true), AssertedBody),
|
|
34
|
+
( TextBody == AssertedBody -> Agree = agree ; Agree = differ ).
|
|
35
|
+
|
|
36
|
+
%% goal: cut_is_local(X0)
|
|
37
|
+
|
|
38
|
+
% 7.8.3: a cut in the argument of call/1 is local to that call, so it must not
|
|
39
|
+
% prune the remaining clauses of cut_scope/1.
|
|
40
|
+
cut_is_local(Solutions) :-
|
|
41
|
+
findall(taken, cut_scope(!), Solutions).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
% Procedures defined by a Prolog text are static unless a dynamic/1 directive
|
|
2
|
+
% makes them public. clause/2 inspects public procedures and raises a
|
|
3
|
+
% permission error for static ones, in every execution mode.
|
|
4
|
+
% https://github.com/eyereasoner/eyeprolog/issues/96
|
|
5
|
+
|
|
6
|
+
:- dynamic(elk/1).
|
|
7
|
+
elk(X) :- moose(X).
|
|
8
|
+
|
|
9
|
+
moose(bertha).
|
|
10
|
+
|
|
11
|
+
%% goal: public_clause(X0)
|
|
12
|
+
|
|
13
|
+
public_clause(Body) :-
|
|
14
|
+
clause(elk(bertha), Body).
|
|
15
|
+
|
|
16
|
+
%% goal: private_clause(X0)
|
|
17
|
+
|
|
18
|
+
private_clause(Culprit) :-
|
|
19
|
+
catch(clause(moose(_), _), error(Formal, _), true),
|
|
20
|
+
Formal = permission_error(access, private_procedure, Culprit).
|
|
21
|
+
|
|
22
|
+
%% goal: asserted_clause(X0)
|
|
23
|
+
|
|
24
|
+
asserted_clause(Body) :-
|
|
25
|
+
assertz(caribou(rudolf)),
|
|
26
|
+
clause(caribou(_), Body).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
% A procedure defined by a Prolog text is static, and therefore private, unless
|
|
2
|
+
% a dynamic/1 directive makes it public (ISO 7.5.2, 8.9). clause/2 may only
|
|
3
|
+
% inspect a public procedure (ISO 8.8.1.3), so reading a static user procedure
|
|
4
|
+
% is a permission error rather than a source of solutions.
|
|
5
|
+
% https://github.com/eyereasoner/eyeprolog/issues/96
|
|
6
|
+
%% goal: answer
|
|
7
|
+
|
|
8
|
+
elk(X) :- moose(X).
|
|
9
|
+
|
|
10
|
+
moose(bertha).
|
|
11
|
+
|
|
12
|
+
answer :-
|
|
13
|
+
clause(elk(N), Body).
|
|
@@ -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(access, private_procedure), /(elk, 1))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(permission_error(modify, static_procedure), /(clause, 2))
|
package/test/run-regression.mjs
CHANGED
|
@@ -4851,6 +4851,212 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
|
|
|
4851
4851
|
assertEqual(answers.length, 1, 'dynamic clause answer count');
|
|
4852
4852
|
},
|
|
4853
4853
|
},
|
|
4854
|
+
{
|
|
4855
|
+
// ISO 7.6.2 a/b, checked against the 8.8.1.4 worked example for legs/2.
|
|
4856
|
+
name: 'preparing a Prolog text converts variable body goals to call/1',
|
|
4857
|
+
run: () => {
|
|
4858
|
+
const program = Program.parse(':- dynamic(legs/2).\nlegs(A, 7) :- A, call(A).\n');
|
|
4859
|
+
const solver = new Solver(program, {});
|
|
4860
|
+
const goal = parseGoalText('clause(legs(c, 7), Body)');
|
|
4861
|
+
const answers = [...solver.solve([goal], new Env(), 0)];
|
|
4862
|
+
assertEqual(answers.length, 1, 'clause answer count');
|
|
4863
|
+
assertEqual(termToString(copyResolved(goal.args[1], answers[0])), '(call(c), call(c))', 'converted body');
|
|
4864
|
+
},
|
|
4865
|
+
},
|
|
4866
|
+
{
|
|
4867
|
+
name: 'preparation and assertz/1 agree on clause body conversion',
|
|
4868
|
+
run: () => {
|
|
4869
|
+
const fromText = Program.parse(':- dynamic(p/1).\np(G) :- G.\n');
|
|
4870
|
+
const textGoal = parseGoalText('clause(p(true), B)');
|
|
4871
|
+
const textAnswers = [...new Solver(fromText, {}).solve([textGoal], new Env(), 0)];
|
|
4872
|
+
const asserted = Program.parse('run(B) :- assertz((q(G) :- G)), clause(q(true), B).\n');
|
|
4873
|
+
const assertGoal = parseGoalText('run(B)');
|
|
4874
|
+
const assertAnswers = [...new Solver(asserted, {}).solve([assertGoal], new Env(), 0)];
|
|
4875
|
+
assertEqual(textAnswers.length, 1, 'text clause answer count');
|
|
4876
|
+
assertEqual(assertAnswers.length, 1, 'asserted clause answer count');
|
|
4877
|
+
assertEqual(
|
|
4878
|
+
termToString(copyResolved(textGoal.args[1], textAnswers[0])),
|
|
4879
|
+
termToString(copyResolved(assertGoal.args[0], assertAnswers[0])),
|
|
4880
|
+
'converted bodies agree',
|
|
4881
|
+
);
|
|
4882
|
+
},
|
|
4883
|
+
},
|
|
4884
|
+
{
|
|
4885
|
+
// 7.6.2 b converts the arguments of ;/2 and ->/2 recursively.
|
|
4886
|
+
name: 'body conversion recurses through disjunction and if-then',
|
|
4887
|
+
run: () => {
|
|
4888
|
+
const program = Program.parse(':- dynamic(r/3).\nr(A, B, C) :- (A ; B), (C -> true ; true).\n');
|
|
4889
|
+
const solver = new Solver(program, {});
|
|
4890
|
+
const goal = parseGoalText('clause(r(a, b, c), Body)');
|
|
4891
|
+
const answers = [...solver.solve([goal], new Env(), 0)];
|
|
4892
|
+
assertEqual(answers.length, 1, 'clause answer count');
|
|
4893
|
+
assertEqual(
|
|
4894
|
+
termToString(copyResolved(goal.args[1], answers[0])),
|
|
4895
|
+
"(';'(call(a), call(b)), ';'(->(call(c), true), true))",
|
|
4896
|
+
'recursively converted body',
|
|
4897
|
+
);
|
|
4898
|
+
},
|
|
4899
|
+
},
|
|
4900
|
+
{
|
|
4901
|
+
// 7.8.3: a cut inside call/1 is local, so the remaining clauses survive.
|
|
4902
|
+
name: 'a variable body goal from a Prolog text keeps cut local',
|
|
4903
|
+
run: () => {
|
|
4904
|
+
const program = Program.parse('s(G) :- G.\ns(_) :- true.\n');
|
|
4905
|
+
const solver = new Solver(program, {});
|
|
4906
|
+
const answers = [...solver.solve([parseGoalText('s(!)')], new Env(), 0)];
|
|
4907
|
+
assertEqual(answers.length, 2, 'cut did not prune the second clause');
|
|
4908
|
+
},
|
|
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
|
+
// https://github.com/eyereasoner/eyeprolog/issues/96
|
|
4992
|
+
name: 'clause/2 keeps static procedures private in the default mode',
|
|
4993
|
+
run: () => {
|
|
4994
|
+
const program = Program.parse('elk(X) :- moose(X).\nmoose(bertha).\n');
|
|
4995
|
+
const solver = new Solver(program, {});
|
|
4996
|
+
let caught = null;
|
|
4997
|
+
try {
|
|
4998
|
+
[...solver.solve([parseGoalText('clause(elk(N), Body)')], new Env(), 0)];
|
|
4999
|
+
} catch (error) {
|
|
5000
|
+
caught = error;
|
|
5001
|
+
}
|
|
5002
|
+
if (!caught) throw new Error('clause/2 unexpectedly inspected a static procedure');
|
|
5003
|
+
assertEqual(caught.formal, 'permission_error(access, private_procedure)', 'formal error');
|
|
5004
|
+
assertEqual(termToString(caught.culprit), '/(elk, 1)', 'culprit predicate indicator');
|
|
5005
|
+
},
|
|
5006
|
+
},
|
|
5007
|
+
{
|
|
5008
|
+
name: 'clause/2 inspects a procedure declared dynamic in the default mode',
|
|
5009
|
+
run: () => {
|
|
5010
|
+
const program = Program.parse(':- dynamic(elk/1).\nelk(X) :- moose(X).\nmoose(bertha).\n');
|
|
5011
|
+
const solver = new Solver(program, {});
|
|
5012
|
+
const answers = [...solver.solve([parseGoalText('clause(elk(N), Body)')], new Env(), 0)];
|
|
5013
|
+
assertEqual(answers.length, 1, 'dynamic clause answer count');
|
|
5014
|
+
},
|
|
5015
|
+
},
|
|
5016
|
+
{
|
|
5017
|
+
name: 'clause/2 inspects procedures created by assertz/1 in the default mode',
|
|
5018
|
+
run: () => {
|
|
5019
|
+
const program = Program.parse('run(Body) :- assertz(elk(bertha)), clause(elk(_), Body).\n');
|
|
5020
|
+
const solver = new Solver(program, {});
|
|
5021
|
+
const answers = [...solver.solve([parseGoalText('run(Body)')], new Env(), 0)];
|
|
5022
|
+
assertEqual(answers.length, 1, 'asserted clause answer count');
|
|
5023
|
+
},
|
|
5024
|
+
},
|
|
5025
|
+
{
|
|
5026
|
+
name: 'clause/2 and assertz/1 agree on which procedures are static',
|
|
5027
|
+
run: () => {
|
|
5028
|
+
const program = Program.parse('elk(bertha).\n');
|
|
5029
|
+
const solver = new Solver(program, {});
|
|
5030
|
+
const formals = [];
|
|
5031
|
+
for (const goal of ['clause(elk(_), Body)', 'assertz(elk(clara))', 'retract(elk(bertha))']) {
|
|
5032
|
+
try {
|
|
5033
|
+
[...solver.solve([parseGoalText(goal)], new Env(), 0)];
|
|
5034
|
+
formals.push(null);
|
|
5035
|
+
} catch (error) {
|
|
5036
|
+
formals.push(error.formal);
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
assertEqual(formals[0], 'permission_error(access, private_procedure)', 'clause/2 access');
|
|
5040
|
+
assertEqual(formals[1], 'permission_error(modify, static_procedure)', 'assertz/1 modification');
|
|
5041
|
+
assertEqual(formals[2], 'permission_error(modify, static_procedure)', 'retract/1 modification');
|
|
5042
|
+
},
|
|
5043
|
+
},
|
|
5044
|
+
{
|
|
5045
|
+
// A static procedure stays private even when clause/2 is also given a
|
|
5046
|
+
// non-callable body, matching the existing strict-mode error ordering.
|
|
5047
|
+
name: 'clause/2 reports private access before body callability',
|
|
5048
|
+
run: () => {
|
|
5049
|
+
const program = Program.parse('elk(bertha).\n');
|
|
5050
|
+
const solver = new Solver(program, {});
|
|
5051
|
+
let caught = null;
|
|
5052
|
+
try {
|
|
5053
|
+
[...solver.solve([parseGoalText('clause(elk(_), 4)')], new Env(), 0)];
|
|
5054
|
+
} catch (error) {
|
|
5055
|
+
caught = error;
|
|
5056
|
+
}
|
|
5057
|
+
assertEqual(caught?.formal, 'permission_error(access, private_procedure)', 'formal error');
|
|
5058
|
+
},
|
|
5059
|
+
},
|
|
4854
5060
|
{
|
|
4855
5061
|
name: 'strict ISO core mode disables normal-profile recursion planning',
|
|
4856
5062
|
run: () => {
|
|
@@ -8044,14 +8250,16 @@ function whiteBoxCases() {
|
|
|
8044
8250
|
assertEqual(termToString(candidates.primary[0].head, new Env(), true), 'row(a0, b0, c0, first)', 'first head');
|
|
8045
8251
|
assertEqual(termToString(candidates.primary[1].head, new Env(), true), 'row(a0, X, c0, wildcard)', 'wildcard head');
|
|
8046
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.
|
|
8047
8255
|
const variableHeavy = Program.parse(Array.from(
|
|
8048
8256
|
{ length: 12 },
|
|
8049
|
-
(_, index) => `
|
|
8257
|
+
(_, index) => `gate(X${index}, Y${index}, value${index}).`,
|
|
8050
8258
|
).join('\n'));
|
|
8051
|
-
const
|
|
8052
|
-
selectClauseCandidates(
|
|
8053
|
-
assertEqual(
|
|
8054
|
-
assertEqual(
|
|
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');
|
|
8055
8263
|
},
|
|
8056
8264
|
},
|
|
8057
8265
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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`. Access to built-
|
|
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.
|
|
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.
|
|
@@ -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
|
|
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',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|