eyeprolog 1.3.77 → 1.3.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -346
- package/package.json +1 -1
- package/src/iso.js +43 -10
- package/src/solver.js +70 -19
- package/src/standard-library.js +31 -12
- package/test/run-regression.mjs +96 -11
- package/the-art-of-eyeprolog.md +39 -26
package/README.md
CHANGED
|
@@ -12,28 +12,22 @@ EyeProlog turns portable ISO Prolog programs into answers and inspectable proofs
|
|
|
12
12
|
<strong>Click the cover to read <em>The Art of EyeProlog</em>.</strong>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
The book is the reference for the language, command line, JavaScript API,
|
|
20
|
-
examples, proofs, conformance, and implementation.
|
|
15
|
+
The single implementation reference is [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
|
|
16
|
+
It documents the language, built-ins, libraries, command line, JavaScript API,
|
|
17
|
+
examples, proofs, conformance profile, and implementation.
|
|
21
18
|
|
|
22
19
|
## Quick start
|
|
23
20
|
|
|
24
|
-
EyeProlog requires Node.js 18 or newer
|
|
25
|
-
installing:
|
|
21
|
+
EyeProlog requires Node.js 18 or newer:
|
|
26
22
|
|
|
27
23
|
```sh
|
|
28
24
|
node --version
|
|
29
25
|
```
|
|
30
26
|
|
|
31
|
-
If
|
|
32
|
-
[official Node.js download](https://nodejs.org/en/download)
|
|
33
|
-
Distribution packages can provide an older Node.js even on a current operating
|
|
34
|
-
system.
|
|
27
|
+
If necessary, upgrade through a Node version manager or the
|
|
28
|
+
[official Node.js download](https://nodejs.org/en/download).
|
|
35
29
|
|
|
36
|
-
Run EyeProlog without
|
|
30
|
+
Run EyeProlog without installing it globally:
|
|
37
31
|
|
|
38
32
|
```sh
|
|
39
33
|
npx --yes eyeprolog
|
|
@@ -45,8 +39,7 @@ npx --yes eyeprolog
|
|
|
45
39
|
?- halt.
|
|
46
40
|
```
|
|
47
41
|
|
|
48
|
-
For a persistent
|
|
49
|
-
under a user-owned prefix and put that prefix's `bin` directory on `PATH`:
|
|
42
|
+
For a persistent command, use a user-owned npm prefix:
|
|
50
43
|
|
|
51
44
|
```sh
|
|
52
45
|
npm install --global --prefix "$HOME/.local" eyeprolog
|
|
@@ -54,335 +47,26 @@ export PATH="$HOME/.local/bin:$PATH"
|
|
|
54
47
|
eyeprolog
|
|
55
48
|
```
|
|
56
49
|
|
|
57
|
-
Add the `PATH` export to your shell startup file
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
also recommends a Node version manager or a user-owned npm prefix.
|
|
50
|
+
Add the `PATH` export to your shell startup file. Do not use `sudo npm install`;
|
|
51
|
+
npm's [EACCES guidance](https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally/)
|
|
52
|
+
also recommends a Node version manager or a user-owned prefix.
|
|
61
53
|
|
|
62
|
-
|
|
54
|
+
Run a program non-interactively:
|
|
63
55
|
|
|
64
56
|
```sh
|
|
65
57
|
printf 'human(socrates).\nmortal(X) :- human(X).\n' |
|
|
66
58
|
npx --yes eyeprolog --proof --goal 'mortal(socrates)' -
|
|
67
59
|
```
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
Double-quoted text follows the ISO `double_quotes` flag and defaults to a
|
|
71
|
-
proper list of one-character atoms (`chars`), matching Trealla and Scryer.
|
|
72
|
-
|
|
73
|
-
Portable unit tests can be embedded as quads—a query followed by its expected
|
|
74
|
-
top-level answer—and run with `eyeprolog --quads program.pl`:
|
|
75
|
-
|
|
76
|
-
```prolog
|
|
77
|
-
member_test ?- member(X, [prolog, logic]).
|
|
78
|
-
X = prolog
|
|
79
|
-
; X = logic.
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
A label is simply the first argument of the ordinary `(?-)/2` term, so it may
|
|
83
|
-
use any normal Prolog term syntax; EyeProlog only requires it to be ground when
|
|
84
|
-
the quad is checked. A non-ground label is a quad failure, not a source syntax
|
|
85
|
-
error, and later quads still run. When a query has multiple indented answer
|
|
86
|
-
descriptions, each description is checked and counted independently, so one
|
|
87
|
-
failed expectation does not prevent the later ones from running. Following the
|
|
88
|
-
Trealla quad convention, `sto` declares that the query is subject to occurs-check.
|
|
89
|
-
EyeProlog now uses an occurs-check event observed during the query's ordinary
|
|
90
|
-
execution as positive STO evidence, and rejects `sto` when a finite execution
|
|
91
|
-
completes without such an event (for example `?- true. sto.`). If execution is
|
|
92
|
-
cut short by a search/resource boundary, the STO claim remains conservatively
|
|
93
|
-
unverified rather than being guessed.
|
|
94
|
-
For nontermination expectations, `loops` is kept distinct from resource
|
|
95
|
-
exhaustion: structural loop evidence may satisfy `loops`, while a bounded
|
|
96
|
-
search that cannot establish the requested answer sequence is reported as
|
|
97
|
-
`UNDECIDED`. In CLI quad mode, failures use exit status `1`; if there are no
|
|
98
|
-
failures but at least one undecided quad, the exit status is `2`.
|
|
99
|
-
|
|
100
|
-
## Tabling and well-founded negation
|
|
101
|
-
|
|
102
|
-
In normal mode, EyeProlog automatically tables eligible positive recursive
|
|
103
|
-
predicates. For large finite, function-free Datalog dependency cones it may use
|
|
104
|
-
one shared relation-wide table, so an open query such as `tc(X, Y)` computes a
|
|
105
|
-
finite closure once and bound recursive calls can reuse indexed answers. The
|
|
106
|
-
choice of when to use relation-wide tabling is an implementation optimization,
|
|
107
|
-
not a language-level threshold or directive.
|
|
108
|
-
|
|
109
|
-
Recursive nonterminals reached through `phrase/2-3` use a dedicated table
|
|
110
|
-
scope keyed by the complete invocation rather than the caller's general memo.
|
|
111
|
-
When a new input is seen, recursive DCGs that static analysis proves consume a
|
|
112
|
-
list tail on every recursive step run directly without automatic tabling; this
|
|
113
|
-
avoids building and retaining a family of suffix tables that cannot help the
|
|
114
|
-
next distinct input. If the same `phrase/2-3` invocation repeats, normal tabling
|
|
115
|
-
is enabled again and its small completed table can be reused. The retained
|
|
116
|
-
phrase table is also bounded, with active fixed points never evicted. This
|
|
117
|
-
keeps both long-running filters such as `\+ phrase((..., Pattern), Sequence)`
|
|
118
|
-
and repeated same-input probes bounded without turning memory safety into a
|
|
119
|
-
per-call cache-eviction cost.
|
|
120
|
-
|
|
121
|
-
Finite-tree unification also accepts an internal proven-nonoccurrence hint: when
|
|
122
|
-
the solver can prove that a variable cannot occur in the value it is about to
|
|
123
|
-
receive, that binding skips an otherwise redundant occurs traversal. The main
|
|
124
|
-
source-level case is conservative first use in a freshly renamed clause, inspired
|
|
125
|
-
by the local-variable optimization used by WAM-family systems; native construction
|
|
126
|
-
paths such as relational `length/2` reuse the same unifier mechanism. Repeated
|
|
127
|
-
variables, variables seen earlier in the clause, and ordinary public unification
|
|
128
|
-
remain fully occurs-checked. This is a proof local to one binding, not a WAM-style
|
|
129
|
-
local/global variable stack. `phrase/2` likewise supplies its fixed `[]`
|
|
130
|
-
remainder directly to the grammar; `phrase/3` retains its delayed final output
|
|
131
|
-
unification for steadfastness.
|
|
132
|
-
|
|
133
|
-
### Delayed disequality with `dif/2`
|
|
134
|
-
|
|
135
|
-
The normal EyeProlog runtime provides `dif/2` as a conforming extension;
|
|
136
|
-
`--iso-strict` remains limited to ISO/IEC 13211-1 and does not expose it.
|
|
137
|
-
Unlike `\=/2`, which only tests whether two terms are non-unifiable *now*,
|
|
138
|
-
`dif/2` can succeed with a residual disequality constraint and reject a later
|
|
139
|
-
binding that would make the terms identical. For example,
|
|
140
|
-
`dif(X, Y), X = Y` fails, while `dif(X-Y, 1-2), X = Y, Y = 1` succeeds once
|
|
141
|
-
the structural constraint becomes provably satisfied.
|
|
142
|
-
|
|
143
|
-
The implementation uses backtracking-safe annotated variables in `Env`. A
|
|
144
|
-
pending constraint is indexed by every currently unbound variable it mentions;
|
|
145
|
-
variable aliasing and later bindings reindex the annotation, entailed constraints
|
|
146
|
-
are removed, and violated constraints make unification fail. The interactive
|
|
147
|
-
top level includes pending `dif/2` goals in residual answers. See
|
|
148
|
-
[`examples/dif-constraints.pl`](examples/dif-constraints.pl).
|
|
149
|
-
|
|
150
|
-
### Attributed variables and CLP(Z)
|
|
151
|
-
|
|
152
|
-
`library(atts)` exposes Scryer-style attributed-variable operations on the same
|
|
153
|
-
persistent `Env` machinery used by `dif/2`: `put_atts/2`, `get_atts/2`,
|
|
154
|
-
`put_attr/3`, `get_attr/3`, `del_attr/2`, `term_attributed_variables/2`, and
|
|
155
|
-
`call_residue_vars/2`. The latter reports variables whose residual attributes or
|
|
156
|
-
constraints were created or modified by a goal, including native `dif/2` residues.
|
|
157
|
-
`:- attribute ...` declarations are accepted, `verify_attributes/3` runs before
|
|
158
|
-
an attributed binding is committed, and the hook's returned goals run after
|
|
159
|
-
that binding. Attribute state follows variable representatives and ordinary
|
|
160
|
-
Prolog backtracking, and the REPL projects module `attribute_goals//1` hooks as
|
|
161
|
-
residual goals. See [`examples/attributed-variables.pl`](examples/attributed-variables.pl).
|
|
162
|
-
|
|
163
|
-
`library(clpz)` is the MIT-licensed Scryer Prolog implementation of CLP(Z),
|
|
164
|
-
bundled as `src/lib/clpz.pl`. It provides relational integer arithmetic,
|
|
165
|
-
finite-domain constraints, reification, labeling, global constraints such as
|
|
166
|
-
`all_distinct/1`, `global_cardinality/2,3`, `cumulative/1,2`, `circuit/1`,
|
|
167
|
-
`disjoint2/1`, and `automaton/3,8`, plus domain reflection predicates. The
|
|
168
|
-
bundled source is synchronized with Scryer commit
|
|
169
|
-
`e3df91e25f8a09ee942c04e8baef553bba5c6110` (Git blob
|
|
170
|
-
`806445c11e14c8b2515f3de7f309e0ac04d9ad04`).
|
|
171
|
-
|
|
172
|
-
EyeProlog's runtime supplies the generic facilities used by that Prolog source:
|
|
173
|
-
attributed variables, user `term_expansion/2` and `goal_expansion/2`, DCG
|
|
174
|
-
expansion through `expand_term/2`, module-aware meta-calls, a backtrackable
|
|
175
|
-
blackboard, and the support modules `assoc`, `pairs`, `between`, `dcgs`, `terms`,
|
|
176
|
-
`error`, `si`, `freeze`, `arithmetic`, `debug`, and `format`.
|
|
177
|
-
|
|
178
|
-
Recursion through negation is explicit. EyeProlog provides `tnot/1` for
|
|
179
|
-
well-founded negation over finite, range-restricted, function-free Datalog
|
|
180
|
-
components. Ordinary `\+/1` remains negation-as-failure and is not silently
|
|
181
|
-
reinterpreted as WFS.
|
|
182
|
-
|
|
183
|
-
```prolog
|
|
184
|
-
move(1, 2).
|
|
185
|
-
move(2, 1).
|
|
186
|
-
win(X) :- move(X, Y), tnot(win(Y)).
|
|
187
|
-
|
|
188
|
-
%% goal: win(X)
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
A cycle through `tnot/1` can leave atoms *undefined* rather than true or false.
|
|
192
|
-
EyeProlog exposes those as conditional successes so they can participate in
|
|
193
|
-
collectors such as `findall/3`; it does not currently expose a residual-program
|
|
194
|
-
API. Direct `tnot/1` calls must be ground. Variables used in an eligible WFS
|
|
195
|
-
rule are range-restricted by positive body literals before they are negated.
|
|
196
|
-
|
|
197
|
-
The normal-mode statistics interface includes `wfs_fixpoint_rounds` and
|
|
198
|
-
`wfs_undefined_answers`:
|
|
199
|
-
|
|
200
|
-
```prolog
|
|
201
|
-
statistics(wfs_fixpoint_rounds, Rounds).
|
|
202
|
-
statistics(wfs_undefined_answers, UndefinedObservations).
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Both automatic tabling and `tnot/1` are EyeProlog extensions. Strict ISO mode
|
|
206
|
-
disables automatic tabling and does not provide `tnot/1`.
|
|
207
|
-
|
|
208
|
-
## Cleanup-aware control
|
|
209
|
-
|
|
210
|
-
Normal mode provides `call_cleanup/2` and `setup_call_cleanup/3` for resource
|
|
211
|
-
lifetimes that follow Prolog search. Cleanup runs exactly once when the protected
|
|
212
|
-
goal completes deterministically, is exhausted, is cut or otherwise pruned, the
|
|
213
|
-
top level stops answer enumeration, or an exception unwinds the search.
|
|
214
|
-
`setup_call_cleanup/3` runs Setup once and installs Cleanup only after Setup
|
|
215
|
-
succeeds. On ordinary pruning Cleanup sees the current goal bindings; during
|
|
216
|
-
exception unwinding bindings made by the protected goal have already been
|
|
217
|
-
unwound. Cleanup failure is ignored, and an exception already being propagated
|
|
218
|
-
takes precedence over a cleanup exception. Nested cleanups run inside-out.
|
|
219
|
-
|
|
220
|
-
These predicates are EyeProlog normal-mode extensions and are absent from
|
|
221
|
-
`--iso-strict`. Their implementation is lifecycle-aware: the interactive top
|
|
222
|
-
level can report a remaining choicepoint without speculatively requesting the
|
|
223
|
-
next solution, while abandoning that choicepoint still closes protected
|
|
224
|
-
resources.
|
|
225
|
-
|
|
226
|
-
## OpenRuleBench portable profile
|
|
227
|
-
|
|
228
|
-
The `openrulebench/` directory contains a deterministic four-engine adaptation
|
|
229
|
-
for EyeProlog, Trealla, Scryer, and SWI-Prolog. Its default `portable` profile
|
|
230
|
-
keeps the characteristic joins, recursive closures, and WFS cases while
|
|
231
|
-
avoiding benchmark sizes that require multi-gigabyte collectors on some
|
|
232
|
-
engines. Run EyeProlog's complete profile with:
|
|
233
|
-
|
|
234
|
-
```sh
|
|
235
|
-
./openrulebench/run-eyeprolog.mjs
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
The benchmark README records the expected answer counts. Timing values are
|
|
239
|
-
machine-dependent; use the same generated profile when comparing engines.
|
|
240
|
-
|
|
241
|
-
## Strict ISO/IEC 13211-1 core
|
|
242
|
-
|
|
243
|
-
For portability and conformance work, run the Part 1 core with Technical
|
|
244
|
-
Corrigenda 1–3 in strict mode:
|
|
245
|
-
|
|
246
|
-
```sh
|
|
247
|
-
eyeprolog --iso-strict
|
|
248
|
-
eyeprolog --iso-strict --goal 'p(X)' program.pl
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
The equivalent JavaScript option is `isoStrict: true`. Strict mode keeps
|
|
252
|
-
EyeProlog's documented implementation-defined Unicode scalar processor
|
|
253
|
-
character set, while excluding normal-profile language extensions such as Part
|
|
254
|
-
2 module compatibility forms, Part 3 DCG expansion, quads, extra libraries,
|
|
255
|
-
automatic tabling, and non-standard flags/control facilities. Normal mode is
|
|
256
|
-
unchanged.
|
|
257
|
-
|
|
258
|
-
This README intentionally stays at the project-overview level. The detailed
|
|
259
|
-
implementation reference is [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
|
|
260
|
-
The current Part 1 audit status and closure criteria are recorded in
|
|
261
|
-
[`test/conformance/ISO-COMPLIANCE.md`](test/conformance/ISO-COMPLIANCE.md);
|
|
262
|
-
implementation-defined decisions are indexed in
|
|
263
|
-
[`ISO-IMPLEMENTATION-DEFINED.md`](test/conformance/ISO-IMPLEMENTATION-DEFINED.md). The complete
|
|
264
|
-
vendored WG17 syntax corpus and the strict ISO regression suite run as release
|
|
265
|
-
gates.
|
|
266
|
-
|
|
267
|
-
The release-facing Part 1 audit ledger has explicit dispositions for its tracked requirements. This implementation evidence is not an independent ISO certification.
|
|
268
|
-
|
|
269
|
-
## Module and definite clause grammar compatibility profiles
|
|
270
|
-
|
|
271
|
-
Normal EyeProlog supports the widely used `module/2`, `use_module/1-2`,
|
|
272
|
-
`meta_predicate/1`, and `Module:Goal` interface reflected in later WG17 module
|
|
273
|
-
amendment work. This is a practical module compatibility profile; EyeProlog
|
|
274
|
-
does **not** currently claim a clause-by-clause implementation or certification
|
|
275
|
-
of the complete ISO/IEC 13211-2:2000 module model.
|
|
276
|
-
|
|
277
|
-
Definite clause grammar support follows the ISO/IEC TS 13211-3 grammar-rule and
|
|
278
|
-
`phrase/2-3` model and is exercised by the conformance corpus. As with Part 1,
|
|
279
|
-
that implementation evidence is not an independent certification of every
|
|
280
|
-
Part 3 requirement. For example:
|
|
281
|
-
|
|
282
|
-
```prolog
|
|
283
|
-
sentence --> [hello], noun.
|
|
284
|
-
noun --> [world] | [prolog].
|
|
285
|
-
|
|
286
|
-
%% goal: phrase(sentence, Words)
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
For a larger bidirectional example, see
|
|
290
|
-
[`examples/dcg-expression-language.pl`](examples/dcg-expression-language.pl).
|
|
291
|
-
It parses precedence-sensitive arithmetic into an AST, evaluates expressions
|
|
292
|
-
with variables, generates tokens back from an AST with only the necessary
|
|
293
|
-
parentheses, round-trips the generated form, and demonstrates `phrase/3`
|
|
294
|
-
remainder handling. The example uses accumulator nonterminals for
|
|
295
|
-
left-associative operators, so state is handed repeatedly from one nonterminal
|
|
296
|
-
to the next rather than hidden in host code.
|
|
297
|
-
|
|
298
|
-
Deep finite DCG traversal is kept relational but does not have to consume one
|
|
299
|
-
general solver frame per token. In particular, the interoperable `... //0`
|
|
300
|
-
helper from `library(iso_ext)` can scan a finite compact list iteratively, and a
|
|
301
|
-
following grammar that is statically known to leave the DCG state unchanged can
|
|
302
|
-
be continued without rebuilding a full clause-resolution frame for every
|
|
303
|
-
suffix. Open and remainder-producing uses still retain the ordinary DCG
|
|
304
|
-
solutions and backtracking behavior.
|
|
61
|
+
## Links
|
|
305
62
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
`library(atts)`, or `library(clpz)`.
|
|
314
|
-
Portable text
|
|
315
|
-
predicates use ISO atoms or character lists. The old catch-all
|
|
316
|
-
`library(eyeprolog)` module is no longer needed.
|
|
317
|
-
|
|
318
|
-
## Trealla and Scryer interoperability
|
|
319
|
-
|
|
320
|
-
EyeProlog keeps a conservative source-level interoperability profile separate
|
|
321
|
-
from the larger EyeProlog library surface. `library(lists)` follows the common
|
|
322
|
-
Trealla/Scryer organization for predicates such as `member/2`, `memberchk/2`,
|
|
323
|
-
`append/2-3`, `nth0/3-4`, `nth1/3-4`, `length/2`, `maplist/2-8`, and
|
|
324
|
-
`foldl/4-6`. Its `length/2` remains relational: with both arguments variable,
|
|
325
|
-
`length(Xs, N)` enumerates lists of increasing length together with `N = 0, 1,
|
|
326
|
-
2, ...`. Open-ended generation uses the normal memory guard with recovery
|
|
327
|
-
headroom, so an exhausted finite heap is reported as a catchable
|
|
328
|
-
`resource_error(memory)` instead of degenerating into quadratic list checks.
|
|
329
|
-
|
|
330
|
-
`library(iso_ext)` is also accepted as a common interop module name.
|
|
331
|
-
EyeProlog exports `call_nth/2`, `time/1`, and the DCG helper `... //0` there.
|
|
332
|
-
The latter describes an arbitrary number of input elements and supports the
|
|
333
|
-
nonterminal hand-off benchmark used by the interoperability tests. These common predicates
|
|
334
|
-
may be imported explicitly, while source/CLI/API dependency loading can resolve
|
|
335
|
-
their unqualified forms conservatively. For Trealla-style interactive timing,
|
|
336
|
-
`time/1` is also available directly in the normal EyeProlog runtime; strict ISO
|
|
337
|
-
mode does not expose it.
|
|
338
|
-
`library(lists)`, `library(iso_ext)`, and `library(freeze)` follow the focused
|
|
339
|
-
module organization used by Trealla and Scryer. EyeProlog's legacy
|
|
340
|
-
`library(prologue)` is now a compatibility facade over these canonical modules
|
|
341
|
-
and `library(between)`, so it can be imported before or after the focused
|
|
342
|
-
libraries without duplicate-import errors.
|
|
343
|
-
|
|
344
|
-
`library(lambda)` follows Scryer's higher-order lambda notation, adapted from
|
|
345
|
-
Ulrich Neumerkel's permissively licensed implementation. Importing it installs
|
|
346
|
-
the `+\` operator and enables closures such as `\X^Goal` and
|
|
347
|
-
`Free+\X^Goal`. Parameters are supplied by `call/N`; variables not listed in
|
|
348
|
-
`Free` are copied afresh for each invocation, while explicitly free variables
|
|
349
|
-
remain shared. For example:
|
|
350
|
-
|
|
351
|
-
```prolog
|
|
352
|
-
:- use_module(library(lambda)).
|
|
353
|
-
:- use_module(library(lists)).
|
|
354
|
-
|
|
355
|
-
all_positive(Xs) :- maplist(\X^(X > 0), Xs).
|
|
356
|
-
all_equal(Y, Xs) :- maplist(Y+\X^(X = Y), Xs).
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
The explicit import is intentional: unlike ordinary predicate autoloading, the
|
|
360
|
-
lambda syntax also changes the active operator table.
|
|
361
|
-
|
|
362
|
-
Outside `--iso-strict`, an otherwise undefined unqualified call may autoload a
|
|
363
|
-
predicate only when the interop profile has one canonical EyeProlog provider.
|
|
364
|
-
For example, `member/2` autoloads from `library(lists)`, `call_nth/2` from
|
|
365
|
-
`library(iso_ext)`, `call_residue_vars/2` from `library(atts)`, and `between/3`
|
|
366
|
-
from `library(between)`. Use `--no-autoload` to disable this
|
|
367
|
-
convenience. Strict ISO mode never autoloads library predicates.
|
|
368
|
-
|
|
369
|
-
Use `-w` / `--warnings` to diagnose dependencies outside the interop profile,
|
|
370
|
-
or `--portable` to make such diagnostics fail the run. This catches both
|
|
371
|
-
implementation-specific library names and EyeProlog-only predicates such as
|
|
372
|
-
`set_nth0/4` even when they live in an otherwise common module.
|
|
373
|
-
|
|
374
|
-
Cross-engine smoke tests live in `test/run-interop.mjs`. With Trealla (`tpl`)
|
|
375
|
-
and Scryer (`scryer-prolog`) installed, run:
|
|
376
|
-
|
|
377
|
-
```sh
|
|
378
|
-
npm run test:interop
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
This optional check runs the same portable Towers of Hanoi source under
|
|
382
|
-
EyeProlog, Trealla, and Scryer. The default `npm test` does not require external
|
|
383
|
-
Prolog implementations; it does validate all four generated OpenRuleBench
|
|
384
|
-
source trees and their engine-specific tabling and WFS adaptations. Run those
|
|
385
|
-
fast structural checks separately with `npm run test:openrulebench`.
|
|
63
|
+
- [The Art of EyeProlog](https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog) — complete reference
|
|
64
|
+
- [Why EyeProlog?](https://eyereasoner.github.io/eyeprolog/why-eyeprolog) — project scope and design
|
|
65
|
+
- [Playground](https://eyereasoner.github.io/eyeprolog/playground) — run EyeProlog in a browser
|
|
66
|
+
- [Examples](examples) — runnable programs and checked output
|
|
67
|
+
- [ISO conformance audit](test/conformance/ISO-COMPLIANCE.md) — supported Part 1 profile
|
|
68
|
+
- [Conformance report](conformance-report.md) — generated public corpus summary
|
|
69
|
+
- [OpenRuleBench](openrulebench/README.md) — portable benchmark profile
|
|
386
70
|
|
|
387
71
|
## Development
|
|
388
72
|
|
|
@@ -393,14 +77,4 @@ npm install
|
|
|
393
77
|
npm test
|
|
394
78
|
```
|
|
395
79
|
|
|
396
|
-
The GitHub test workflow runs the complete suite and an npm package dry-run on
|
|
397
|
-
both the minimum supported Node.js 18 release line and Node.js 24. Publishing
|
|
398
|
-
repeats those release checks before uploading the package.
|
|
399
|
-
|
|
400
|
-
The runtime JavaScript modules stay flat under `src/`; the existing `src/lib/`
|
|
401
|
-
directory contains the portable Prolog library modules. See
|
|
402
|
-
[`src/ARCHITECTURE.md`](src/ARCHITECTURE.md) for the source-layer boundaries,
|
|
403
|
-
facade modules, dependency rule, and the requirement that architectural cleanup
|
|
404
|
-
must preserve the existing solver hot paths and benchmark performance.
|
|
405
|
-
|
|
406
80
|
EyeProlog is released under the [MIT License](LICENSE.md).
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -65,9 +65,9 @@ export const isoBuiltins = {
|
|
|
65
65
|
registry.add('=..', 2, univBuiltin, { deterministic: true });
|
|
66
66
|
registry.add('copy_term', 2, copyTermBuiltin, { deterministic: true });
|
|
67
67
|
registry.add('term_variables', 2, termVariablesBuiltin, { deterministic: true });
|
|
68
|
-
registry.add('findall', 3, findallBuiltin);
|
|
69
|
-
registry.add('bagof', 3, bagofBuiltin);
|
|
70
|
-
registry.add('setof', 3, setofBuiltin);
|
|
68
|
+
registry.add('findall', 3, findallBuiltin, { deterministic: true });
|
|
69
|
+
registry.add('bagof', 3, bagofBuiltin, { deterministicWhen: allSolutionsHasAtMostOneGroup });
|
|
70
|
+
registry.add('setof', 3, setofBuiltin, { deterministicWhen: allSolutionsHasAtMostOneGroup });
|
|
71
71
|
registry.add('clause', 2, clauseBuiltin, {
|
|
72
72
|
shouldUse: ({ solver }) => solver.program.findGroup('clause', 2) == null,
|
|
73
73
|
});
|
|
@@ -2367,6 +2367,16 @@ function allSolutionsBuiltin(asSet) {
|
|
|
2367
2367
|
const bagofBuiltin = allSolutionsBuiltin(false);
|
|
2368
2368
|
const setofBuiltin = allSolutionsBuiltin(true);
|
|
2369
2369
|
|
|
2370
|
+
function allSolutionsHasAtMostOneGroup({ goal, env }) {
|
|
2371
|
+
try {
|
|
2372
|
+
const { iterated, quantified } = bagGoalParts(goal.args[1], env);
|
|
2373
|
+
return freeVariables(iterated, goal.args[0], quantified, env).length === 0;
|
|
2374
|
+
} catch (_) {
|
|
2375
|
+
// Leave error selection and reporting to the builtin itself.
|
|
2376
|
+
return false;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2370
2380
|
function callable(term, env) {
|
|
2371
2381
|
term = deref(term, env);
|
|
2372
2382
|
if (term.type === VAR) throw new PrologError('instantiation_error');
|
|
@@ -2823,35 +2833,57 @@ function* negationBuiltin({ solver, goal, env }) {
|
|
|
2823
2833
|
for (const _ of solver.cloneForInnerGoal(1).solve([invoked], env.clone(), 0)) return;
|
|
2824
2834
|
yield env;
|
|
2825
2835
|
}
|
|
2826
|
-
function* solveControlBranch(solver, goal, env) {
|
|
2836
|
+
function* solveControlBranch(solver, goal, env, observePending = null) {
|
|
2837
|
+
const existingStacks = new Set(solver.solveStacks);
|
|
2827
2838
|
for (const answer of solver.solve([callable(goal, env)], env, 0)) {
|
|
2828
2839
|
// A branch answer is internal to its enclosing control construct. The
|
|
2829
2840
|
// surrounding solve will count the completed control goal after the
|
|
2830
2841
|
// builtin yields it. Leaving both counts in place makes a bounded search
|
|
2831
2842
|
// such as once/1 or negation stop before it can observe the branch answer.
|
|
2832
2843
|
if (solver.solutionsSeen > 0) solver.solutionsSeen--;
|
|
2844
|
+
observePending?.(solver.hasPendingAlternatives(existingStacks));
|
|
2833
2845
|
yield answer;
|
|
2834
2846
|
}
|
|
2835
2847
|
}
|
|
2836
|
-
function
|
|
2848
|
+
function disjunctionBuiltin(context) {
|
|
2849
|
+
const state = { pending: true };
|
|
2850
|
+
const iterator = disjunctionSolutions(context, state);
|
|
2851
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
2852
|
+
return iterator;
|
|
2853
|
+
}
|
|
2854
|
+
function* disjunctionSolutions({ solver, goal, env }, state) {
|
|
2837
2855
|
const left = deref(goal.args[0], env);
|
|
2838
2856
|
if (left.type === COMPOUND && left.name === '->' && left.arity === 2) {
|
|
2839
2857
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(left.args[0], env)], env.clone(), 0)) {
|
|
2840
|
-
yield* solveControlBranch(solver, left.args[1], conditionEnv
|
|
2858
|
+
yield* solveControlBranch(solver, left.args[1], conditionEnv,
|
|
2859
|
+
(pending) => { state.pending = pending; });
|
|
2860
|
+
state.pending = false;
|
|
2841
2861
|
return;
|
|
2842
2862
|
}
|
|
2843
|
-
yield* solveControlBranch(solver, goal.args[1], env.clone()
|
|
2863
|
+
yield* solveControlBranch(solver, goal.args[1], env.clone(),
|
|
2864
|
+
(pending) => { state.pending = pending; });
|
|
2865
|
+
state.pending = false;
|
|
2844
2866
|
return;
|
|
2845
2867
|
}
|
|
2846
2868
|
const marker = solver.active[solver.active.length - 1] ?? null;
|
|
2847
2869
|
const markerCutEpoch = marker?.cutEpoch ?? 0;
|
|
2848
2870
|
const solverCutEpoch = solver.cutEpoch;
|
|
2849
|
-
yield* solveControlBranch(solver, goal.args[0], env.clone())
|
|
2871
|
+
yield* solveControlBranch(solver, goal.args[0], env.clone(), (pending) => {
|
|
2872
|
+
const cutThisScope = marker == null
|
|
2873
|
+
? solver.cutEpoch !== solverCutEpoch
|
|
2874
|
+
: (marker.cutEpoch ?? 0) !== markerCutEpoch;
|
|
2875
|
+
state.pending = pending || !cutThisScope;
|
|
2876
|
+
});
|
|
2850
2877
|
const cutThisScope = marker == null
|
|
2851
2878
|
? solver.cutEpoch !== solverCutEpoch
|
|
2852
2879
|
: (marker.cutEpoch ?? 0) !== markerCutEpoch;
|
|
2853
|
-
if (cutThisScope)
|
|
2854
|
-
|
|
2880
|
+
if (cutThisScope) {
|
|
2881
|
+
state.pending = false;
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
yield* solveControlBranch(solver, goal.args[1], env.clone(),
|
|
2885
|
+
(pending) => { state.pending = pending; });
|
|
2886
|
+
state.pending = false;
|
|
2855
2887
|
}
|
|
2856
2888
|
function* ifThenBuiltin({ solver, goal, env }) {
|
|
2857
2889
|
for (const conditionEnv of solver.cloneForInnerGoal(1).solve([callable(goal.args[0], env)], env.clone(), 0)) {
|
|
@@ -2875,6 +2907,7 @@ export class BuiltinRegistry {
|
|
|
2875
2907
|
arity,
|
|
2876
2908
|
handler,
|
|
2877
2909
|
deterministic: options.deterministic ?? false,
|
|
2910
|
+
deterministicWhen: options.deterministicWhen ?? null,
|
|
2878
2911
|
ready: options.ready ?? null,
|
|
2879
2912
|
fallbackWhenNotReady: options.fallbackWhenNotReady ?? false,
|
|
2880
2913
|
shouldUse: options.shouldUse ?? null,
|
package/src/solver.js
CHANGED
|
@@ -607,14 +607,16 @@ export class Solver {
|
|
|
607
607
|
const def = callable ? this.registry.get(goal.name, goal.arity) : null;
|
|
608
608
|
this.active = active;
|
|
609
609
|
if (def && builtinIsReadyOrAuthoritative(def, this, goal, env)) {
|
|
610
|
+
const deterministic = def.deterministic ||
|
|
611
|
+
def.deterministicWhen?.({ solver: this, goal, env }) === true;
|
|
610
612
|
const iterator = def.handler({ solver: this, goal, env });
|
|
611
613
|
const firstResult = iterator.next();
|
|
612
|
-
if (
|
|
614
|
+
if (deterministic) {
|
|
613
615
|
if (!firstResult.done) this.stats.deterministic_builtin_successes++;
|
|
614
616
|
else this.stats.deterministic_builtin_failures++;
|
|
615
617
|
}
|
|
616
618
|
if (firstResult.done) break;
|
|
617
|
-
if (!
|
|
619
|
+
if (!deterministic) {
|
|
618
620
|
stack.push({
|
|
619
621
|
kind: 'resumeBuiltin',
|
|
620
622
|
iterator,
|
|
@@ -819,11 +821,22 @@ export class Solver {
|
|
|
819
821
|
}
|
|
820
822
|
}
|
|
821
823
|
|
|
822
|
-
hasPendingAlternatives() {
|
|
823
|
-
// When solve() is suspended at an answer, active solve stacks contain
|
|
824
|
-
// unexplored work
|
|
825
|
-
//
|
|
826
|
-
|
|
824
|
+
hasPendingAlternatives(excludedStacks = null) {
|
|
825
|
+
// When solve() is suspended at an answer, active solve stacks contain
|
|
826
|
+
// unexplored work or an iterator frame that can prove it just yielded its
|
|
827
|
+
// final answer. The demand-driven REPL uses this without speculatively
|
|
828
|
+
// pulling the next answer. Control builtins can exclude their caller's
|
|
829
|
+
// pre-existing stacks when inspecting a nested branch.
|
|
830
|
+
return this.solveStacks.some((stack) => !excludedStacks?.has(stack) && stack.some((frame) => {
|
|
831
|
+
if (frame.kind === 'resumeBuiltin' &&
|
|
832
|
+
typeof frame.iterator?.hasPendingAlternatives === 'function') {
|
|
833
|
+
return frame.iterator.hasPendingAlternatives();
|
|
834
|
+
}
|
|
835
|
+
if (frame.kind === 'userClause' && headCannotMatch(frame.goal, frame.clause.head, frame.env)) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
return true;
|
|
839
|
+
}));
|
|
827
840
|
}
|
|
828
841
|
|
|
829
842
|
fastCountGoal(goal, env) {
|
|
@@ -1440,7 +1453,10 @@ function bundledBetweenIterator(solver, group, goal, env) {
|
|
|
1440
1453
|
// the generated value in the caller repeatedly revisits all earlier frames.
|
|
1441
1454
|
// Enumerate the canonical bundled relation directly while leaving user
|
|
1442
1455
|
// definitions and non-EyeProlog registries on the ordinary Prolog path.
|
|
1443
|
-
|
|
1456
|
+
const state = { pending: true };
|
|
1457
|
+
const iterator = bundledBetweenSolutions(solver, goal, env, state);
|
|
1458
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
1459
|
+
return iterator;
|
|
1444
1460
|
}
|
|
1445
1461
|
|
|
1446
1462
|
function requireBetweenInteger(term, env) {
|
|
@@ -1452,7 +1468,7 @@ function requireBetweenInteger(term, env) {
|
|
|
1452
1468
|
return BigInt(value.name);
|
|
1453
1469
|
}
|
|
1454
1470
|
|
|
1455
|
-
function* bundledBetweenSolutions(solver, goal, env) {
|
|
1471
|
+
function* bundledBetweenSolutions(solver, goal, env, state) {
|
|
1456
1472
|
const lower = requireBetweenInteger(goal.args[0], env);
|
|
1457
1473
|
const upper = requireBetweenInteger(goal.args[1], env);
|
|
1458
1474
|
const requested = deref(goal.args[2], env);
|
|
@@ -1462,15 +1478,23 @@ function* bundledBetweenSolutions(solver, goal, env) {
|
|
|
1462
1478
|
throw new PrologError('type_error(integer)', requested);
|
|
1463
1479
|
}
|
|
1464
1480
|
const value = BigInt(requested.name);
|
|
1465
|
-
if (value >= lower && value <= upper)
|
|
1481
|
+
if (value >= lower && value <= upper) {
|
|
1482
|
+
state.pending = false;
|
|
1483
|
+
yield env;
|
|
1484
|
+
}
|
|
1485
|
+
state.pending = false;
|
|
1466
1486
|
return;
|
|
1467
1487
|
}
|
|
1468
1488
|
|
|
1469
1489
|
for (let value = lower; value <= upper; value++) {
|
|
1470
1490
|
const next = env.clone();
|
|
1471
1491
|
solver.stats.unify_calls++;
|
|
1472
|
-
if (unify(goal.args[2], numberTerm(value), next))
|
|
1492
|
+
if (unify(goal.args[2], numberTerm(value), next)) {
|
|
1493
|
+
state.pending = value < upper;
|
|
1494
|
+
yield next;
|
|
1495
|
+
}
|
|
1473
1496
|
}
|
|
1497
|
+
state.pending = false;
|
|
1474
1498
|
}
|
|
1475
1499
|
|
|
1476
1500
|
function bundledMemberIterator(solver, group, goal, env) {
|
|
@@ -1487,17 +1511,33 @@ function bundledMemberIterator(solver, group, goal, env) {
|
|
|
1487
1511
|
let cursor = deref(goal.args[1], env);
|
|
1488
1512
|
while (isCons(cursor)) cursor = deref(cursor.args[1], env);
|
|
1489
1513
|
if (!isEmptyList(cursor)) return null;
|
|
1490
|
-
|
|
1514
|
+
const state = { pending: true };
|
|
1515
|
+
const iterator = bundledMemberSolutions(solver, goal, env, state);
|
|
1516
|
+
iterator.hasPendingAlternatives = () => state.pending;
|
|
1517
|
+
return iterator;
|
|
1491
1518
|
}
|
|
1492
1519
|
|
|
1493
|
-
function* bundledMemberSolutions(solver, goal, env) {
|
|
1520
|
+
function* bundledMemberSolutions(solver, goal, env, state) {
|
|
1494
1521
|
let cursor = deref(goal.args[1], env);
|
|
1495
1522
|
while (isCons(cursor)) {
|
|
1523
|
+
const item = cursor.args[0];
|
|
1524
|
+
cursor = deref(cursor.args[1], env);
|
|
1496
1525
|
const next = env.clone();
|
|
1497
1526
|
solver.stats.unify_calls++;
|
|
1498
|
-
if (unify(goal.args[0],
|
|
1527
|
+
if (unify(goal.args[0], item, next)) {
|
|
1528
|
+
state.pending = bundledMemberMayMatch(goal.args[0], cursor, env);
|
|
1529
|
+
yield next;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
state.pending = false;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function bundledMemberMayMatch(target, cursor, env) {
|
|
1536
|
+
while (isCons(cursor)) {
|
|
1537
|
+
if (!goalHeadTermsCannotMatch(target, cursor.args[0], env)) return true;
|
|
1499
1538
|
cursor = deref(cursor.args[1], env);
|
|
1500
1539
|
}
|
|
1540
|
+
return false;
|
|
1501
1541
|
}
|
|
1502
1542
|
|
|
1503
1543
|
function bundledEllipsisPlan(solver, group, goal, rest, env) {
|
|
@@ -2849,11 +2889,22 @@ function headCannotMatch(goal, head, env) {
|
|
|
2849
2889
|
if (goal.type !== COMPOUND || head.type !== COMPOUND) return false;
|
|
2850
2890
|
if (goal.name !== head.name || goal.arity !== head.arity) return true;
|
|
2851
2891
|
for (let i = 0; i < goal.arity; i++) {
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2892
|
+
if (goalHeadTermsCannotMatch(goal.args[i], head.args[i], env)) return true;
|
|
2893
|
+
}
|
|
2894
|
+
return false;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
function goalHeadTermsCannotMatch(goalTerm, headTerm, env) {
|
|
2898
|
+
const pending = [[goalTerm, headTerm]];
|
|
2899
|
+
while (pending.length > 0) {
|
|
2900
|
+
const [goalItem, headItem] = pending.pop();
|
|
2901
|
+
const actual = derefForLocal(goalItem, env);
|
|
2902
|
+
if (actual.type === VAR || headItem.type === VAR) continue;
|
|
2903
|
+
if (actual.type !== headItem.type || actual.name !== headItem.name || actual.arity !== headItem.arity) return true;
|
|
2904
|
+
if (actual.type !== COMPOUND) continue;
|
|
2905
|
+
for (let index = 0; index < actual.arity; index++) {
|
|
2906
|
+
pending.push([actual.args[index], headItem.args[index]]);
|
|
2907
|
+
}
|
|
2857
2908
|
}
|
|
2858
2909
|
return false;
|
|
2859
2910
|
}
|
package/src/standard-library.js
CHANGED
|
@@ -69,25 +69,44 @@ export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
|
69
69
|
'in/2', 'ins/2', 'all_different/1', 'all_distinct/1', 'nvalue/2', 'sum/3',
|
|
70
70
|
'scalar_product/4', 'tuples_in/2', 'labeling/2', 'label/1',
|
|
71
71
|
'indomain/1', 'lex_chain/1', 'serialized/2', 'global_cardinality/2',
|
|
72
|
-
'global_cardinality/3', 'circuit/1', '
|
|
72
|
+
'global_cardinality/3', 'circuit/1', 'cumulative/1', 'cumulative/2',
|
|
73
|
+
'disjoint2/1', 'automaton/3', 'automaton/8', 'chain/2', 'element/3', 'zcompare/3',
|
|
73
74
|
'fd_var/1', 'fd_inf/2', 'fd_sup/2', 'fd_size/2', 'fd_dom/2',
|
|
75
|
+
'clpz_t/2', '#=/3', '#</3',
|
|
74
76
|
]);
|
|
75
77
|
export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
76
78
|
'sumall/3', 'aggregate_min/5', 'aggregate_max/5',
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
79
|
+
'lsb/2', 'msb/2', 'popcount/2',
|
|
80
|
+
'empty_assoc/1', 'assoc_to_list/2', 'get_assoc/3', 'put_assoc/4',
|
|
81
|
+
'between/3', 'gen_int/1', 'gen_nat/1', 'numlist/2', 'numlist/3', 'repeat/1',
|
|
82
|
+
'lt/2', 'gt/2', 'le/2', 'ge/2',
|
|
83
|
+
'difference/3',
|
|
84
|
+
'seq/3', 'seqq/3',
|
|
85
|
+
'debug/1', 'debug/3', 'nodebug/1', 'bb_get/2', 'bb_b_put/2',
|
|
86
|
+
'must_be/2', 'can_be/2', 'instantiation_error/0', 'instantiation_error/1',
|
|
87
|
+
'domain_error/2', 'domain_error/3', 'type_error/2', 'type_error/3',
|
|
88
|
+
'representation_error/1', 'resource_error/1', 'call_with_error_context/2',
|
|
89
|
+
'format/2',
|
|
90
|
+
'forall/2', 'succ/2', 'cfor/3', 'findall/4', 'variant/2', '.../2',
|
|
88
91
|
'^/3', '^/4', '^/5', '^/6', '^/7', '^/8', '^/9', '^/10',
|
|
89
92
|
'\\/1', '\\/2', '\\/3', '\\/4', '\\/5', '\\/6', '\\/7', '\\/8',
|
|
90
93
|
'+\\/2', '+\\/3', '+\\/4', '+\\/5', '+\\/6', '+\\/7', '+\\/8', '+\\/9',
|
|
94
|
+
'member/2', 'memberchk/2', 'select/3', 'append/2', 'append/3', 'last/2', 'same_length/2',
|
|
95
|
+
'nth0/3', 'nth0/4', 'nth1/3', 'nth1/4', 'set_nth0/4', 'take/3', 'drop/3', 'slice/4', 'reverse/2',
|
|
96
|
+
'length/2', 'maplist/2', 'maplist/3', 'maplist/4', 'maplist/5',
|
|
97
|
+
'maplist/6', 'maplist/7', 'maplist/8', 'foldl/4', 'foldl/5', 'foldl/6',
|
|
98
|
+
'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
|
|
99
|
+
'pairs_keys_values/3', 'pairs_keys/2', 'pairs_values/2',
|
|
100
|
+
'group_pairs_by_key/2', 'map_list_to_pairs/3',
|
|
101
|
+
'smallest_divisor_from/3',
|
|
102
|
+
'random/3',
|
|
103
|
+
'atom_si/1', 'integer_si/1', 'atomic_si/1', 'list_si/1', 'character_si/1',
|
|
104
|
+
'term_si/1', 'chars_si/1', 'dif_si/2', 'not_si/1', 'when_si/2',
|
|
105
|
+
'matches/3', 'split/3', 'replace/4', 'lowercase/2', 'uppercase/2', 'trim/2',
|
|
106
|
+
'number_string/2', 'atom_string/2', 'term_string/2', 'string_concat/3',
|
|
107
|
+
'contains/2', 'matches/2', 'join/3', 'substring/4',
|
|
108
|
+
'numbervars/3', 'copy_term_nat/2',
|
|
109
|
+
'uuid/3',
|
|
91
110
|
]);
|
|
92
111
|
export const eyePrologLibraryIndicators = Object.freeze([
|
|
93
112
|
...eyePrologPortableLibraryIndicators,
|
package/test/run-regression.mjs
CHANGED
|
@@ -1826,6 +1826,41 @@ c4 ?- call((!;1)).
|
|
|
1826
1826
|
assertEqual(result.stderr, '', 'stderr');
|
|
1827
1827
|
},
|
|
1828
1828
|
},
|
|
1829
|
+
{
|
|
1830
|
+
name: 'REPL omits exhausted choicepoints without probing future branches (issue #74)',
|
|
1831
|
+
run: () => {
|
|
1832
|
+
const result = runCli([], {
|
|
1833
|
+
input:
|
|
1834
|
+
'X=1;X=2.\n;\n' +
|
|
1835
|
+
'setof(X,true,Xs).\n' +
|
|
1836
|
+
'findall(t,false,Xs).\n' +
|
|
1837
|
+
'use_module(library(lists)).\n' +
|
|
1838
|
+
'member(a,"ba").\n' +
|
|
1839
|
+
'append("a",Xs,Xs0).\n' +
|
|
1840
|
+
'halt.\n',
|
|
1841
|
+
});
|
|
1842
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1843
|
+
assertEqual(result.stdout,
|
|
1844
|
+
'?- X = 1\n' +
|
|
1845
|
+
'; X = 2.\n' +
|
|
1846
|
+
'?- Xs = [_A].\n' +
|
|
1847
|
+
'?- Xs = [].\n' +
|
|
1848
|
+
'?- true.\n' +
|
|
1849
|
+
'?- true.\n' +
|
|
1850
|
+
'?- Xs0 = [a | Xs].\n' +
|
|
1851
|
+
'?- ',
|
|
1852
|
+
'stdout');
|
|
1853
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1854
|
+
|
|
1855
|
+
const grouped = new Solver(Program.parse(''));
|
|
1856
|
+
const groupedSolutions = grouped.solve([
|
|
1857
|
+
parseGoalText('setof(X,(Y=1,X=a;Y=2,X=b),Xs)'),
|
|
1858
|
+
], new Env(), 0);
|
|
1859
|
+
assertEqual(groupedSolutions.next().done, false, 'first grouped setof/3 answer');
|
|
1860
|
+
assertEqual(grouped.hasPendingAlternatives(), true, 'genuine grouped setof/3 choicepoint');
|
|
1861
|
+
groupedSolutions.return();
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1829
1864
|
{
|
|
1830
1865
|
name: 'REPL hides aliases to fresh throw variables while preserving query aliases',
|
|
1831
1866
|
run: () => {
|
|
@@ -3952,7 +3987,15 @@ function documentationSyncCases() {
|
|
|
3952
3987
|
},
|
|
3953
3988
|
{
|
|
3954
3989
|
name: 'book EyeProlog library matches runtime registry',
|
|
3955
|
-
run: () =>
|
|
3990
|
+
run: () => {
|
|
3991
|
+
assertArrayEqual(bookEyePrologLibraryNames(), registeredEyePrologLibraryNames(), 'EyeProlog library predicates');
|
|
3992
|
+
assertArrayEqual(bookLibraryModuleIssues(), [], 'library module export catalog');
|
|
3993
|
+
const book = fs.readFileSync(path.join(packageRoot, 'the-art-of-eyeprolog.md'), 'utf8');
|
|
3994
|
+
assertIncludes(book, `**${eyePrologLibraryIndicators.length} distinct non-ISO library and normal-extension predicate`, 'library predicate count');
|
|
3995
|
+
assertIncludes(book, `**${eyePrologPortableLibraryIndicators.length} are defined entirely as ordinary Prolog clauses**`, 'portable library count');
|
|
3996
|
+
assertIncludes(book, `**${eyePrologNativeLibraryIndicators.length} use host support**`, 'host-supported library count');
|
|
3997
|
+
assertIncludes(book, `**${createDefaultRegistry().defs.size + eyePrologLibraryIndicators.length} distinct predicate indicators**`, 'combined catalog count');
|
|
3998
|
+
},
|
|
3956
3999
|
},
|
|
3957
4000
|
{
|
|
3958
4001
|
name: 'README cover links to the book and the book documents runtime boundaries',
|
|
@@ -3964,11 +4007,10 @@ function documentationSyncCases() {
|
|
|
3964
4007
|
'<a href="https://eyereasoner.github.io/eyeprolog/the-art-of-eyeprolog">\n <img src="book-assets/title-page.svg" alt="Read The Art of EyeProlog"',
|
|
3965
4008
|
'README cover links to the book',
|
|
3966
4009
|
);
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
'src/
|
|
3970
|
-
|
|
3971
|
-
'src/playground-worker.js']) {
|
|
4010
|
+
const documentedSources = ['src/iso.js', 'src/dcg.js', 'src/atts.js', 'src/standard-library.js',
|
|
4011
|
+
...[...standardLibrarySources.values()].map((entry) => entry.filename),
|
|
4012
|
+
'src/playground-worker.js'];
|
|
4013
|
+
for (const filename of documentedSources) {
|
|
3972
4014
|
assertEqual(fs.existsSync(path.join(packageRoot, filename)), true, `${filename} exists`);
|
|
3973
4015
|
assertIncludes(book, filename, `book documents ${filename}`);
|
|
3974
4016
|
}
|
|
@@ -4126,6 +4168,11 @@ function documentationSyncCases() {
|
|
|
4126
4168
|
]) assertIncludes(book, name, `book ${name}`);
|
|
4127
4169
|
assertIncludes(readme, 'implementation reference is [*The Art of EyeProlog*]', 'README book hand-off');
|
|
4128
4170
|
assertIncludes(readme, 'test/conformance/ISO-COMPLIANCE.md', 'README concise audit link');
|
|
4171
|
+
assertEqual(readme.split('\n').length <= 100, true, 'README remains a compact entry point');
|
|
4172
|
+
for (const heading of ['## Tabling', '## Cleanup-aware control', '## Strict ISO',
|
|
4173
|
+
'## Module and definite clause grammar', '## Trealla and Scryer interoperability']) {
|
|
4174
|
+
assertNotIncludes(readme, heading, `README delegates ${heading} to the book`);
|
|
4175
|
+
}
|
|
4129
4176
|
assertEqual(readme.includes('2026-08-23 draft items #73-#76'), false, 'README omits audit-history detail');
|
|
4130
4177
|
assertIncludes(profile, 'Part 1 processor, syntax, semantic, built-in, and arithmetic', 'Why EyeProlog audit state');
|
|
4131
4178
|
},
|
|
@@ -5084,8 +5131,8 @@ answer(ok) :-
|
|
|
5084
5131
|
assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
|
|
5085
5132
|
assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
|
|
5086
5133
|
assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
|
|
5087
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
5088
|
-
assertEqual(eyePrologPortableLibraryIndicators.length,
|
|
5134
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 57, 'public host-supported EyeProlog library count');
|
|
5135
|
+
assertEqual(eyePrologPortableLibraryIndicators.length, 135, 'portable Prolog library count');
|
|
5089
5136
|
assertEqual(eyePrologInteropLibraryIndicators.length, 30, 'cross-implementation interop profile count');
|
|
5090
5137
|
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists,iso_ext,lambda,atts,freeze', 'common explicit library module profile');
|
|
5091
5138
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
@@ -5095,9 +5142,9 @@ answer(ok) :-
|
|
|
5095
5142
|
assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
|
|
5096
5143
|
assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
|
|
5097
5144
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
5098
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
5145
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 57, 'host-supported library count');
|
|
5099
5146
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
|
|
5100
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
5147
|
+
assertEqual(eyePrologLibraryIndicators.length, 192, 'complete non-ISO EyeProlog library surface');
|
|
5101
5148
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
5102
5149
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
5103
5150
|
assertEqual(Boolean(library.get('eyeprolog__call_residue_vars', 2)), true, 'private call_residue_vars adapter is registered for EyeProlog');
|
|
@@ -6257,7 +6304,45 @@ function bookBuiltinNames() {
|
|
|
6257
6304
|
|
|
6258
6305
|
function bookEyePrologLibraryNames() {
|
|
6259
6306
|
const book = fs.readFileSync(path.join(packageRoot, 'the-art-of-eyeprolog.md'), 'utf8');
|
|
6260
|
-
|
|
6307
|
+
const iso = new Set(registeredBuiltinNames());
|
|
6308
|
+
return documentedBuiltinNames(
|
|
6309
|
+
between(book, '<!-- eyeprolog-library-catalog:start -->', '<!-- eyeprolog-library-catalog:end -->'),
|
|
6310
|
+
2,
|
|
6311
|
+
).filter((indicator) => !iso.has(indicator));
|
|
6312
|
+
}
|
|
6313
|
+
|
|
6314
|
+
function bookLibraryModuleIssues() {
|
|
6315
|
+
const book = fs.readFileSync(path.join(packageRoot, 'the-art-of-eyeprolog.md'), 'utf8');
|
|
6316
|
+
const section = between(book, '<!-- eyeprolog-library-catalog:start -->', '<!-- eyeprolog-library-catalog:end -->');
|
|
6317
|
+
const documented = new Map();
|
|
6318
|
+
for (const line of section.split('\n')) {
|
|
6319
|
+
const match = line.match(/^\|\s*`library\(([^)`]+)\)`\s*\|/);
|
|
6320
|
+
if (match == null) continue;
|
|
6321
|
+
documented.set(match[1], documentedBuiltinNames(line, 2));
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
const issues = [];
|
|
6325
|
+
for (const [moduleName, entry] of standardLibrarySources) {
|
|
6326
|
+
const parsed = Program.parse(entry.source);
|
|
6327
|
+
const definition = parsed.modules.get(moduleName);
|
|
6328
|
+
if (definition == null) {
|
|
6329
|
+
issues.push(`missing module declaration for ${moduleName}`);
|
|
6330
|
+
continue;
|
|
6331
|
+
}
|
|
6332
|
+
const actual = [...definition.exports.keys()].sort();
|
|
6333
|
+
const expected = documented.get(moduleName);
|
|
6334
|
+
if (expected == null) {
|
|
6335
|
+
issues.push(`book catalog omits library(${moduleName})`);
|
|
6336
|
+
continue;
|
|
6337
|
+
}
|
|
6338
|
+
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
|
|
6339
|
+
issues.push(`library(${moduleName}) exports: expected ${actual.join(', ')}, documented ${expected.join(', ')}`);
|
|
6340
|
+
}
|
|
6341
|
+
}
|
|
6342
|
+
for (const moduleName of documented.keys()) {
|
|
6343
|
+
if (!standardLibrarySources.has(moduleName)) issues.push(`book catalogs unknown library(${moduleName})`);
|
|
6344
|
+
}
|
|
6345
|
+
return issues.sort();
|
|
6261
6346
|
}
|
|
6262
6347
|
|
|
6263
6348
|
function bookBuiltinSummary() {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6211,16 +6211,18 @@ so side effects occur in Prolog execution order.
|
|
|
6211
6211
|
|
|
6212
6212
|
### The EyeProlog library
|
|
6213
6213
|
|
|
6214
|
-
EyeProlog exposes **
|
|
6215
|
-
indicators in
|
|
6216
|
-
Prolog clauses** in focused modules under
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6214
|
+
EyeProlog exposes **192 distinct non-ISO library and normal-extension predicate
|
|
6215
|
+
indicators** in addition to the 129 indicators in its isolated ISO profile.
|
|
6216
|
+
**135 are defined entirely as ordinary Prolog clauses** in focused modules under
|
|
6217
|
+
`src/lib/`; **57 use host support** for control, attributed variables,
|
|
6218
|
+
constraints, or observability. The ISO and library catalogs therefore cover
|
|
6219
|
+
**321 distinct predicate indicators**. Normal-mode controls and observability
|
|
6220
|
+
relations that do not belong to a library, such as `call_cleanup/2`,
|
|
6221
|
+
`setup_call_cleanup/3`, `statistics/0,2`, and `tnot/1`, are documented in their
|
|
6222
|
+
own sections rather than counted as library predicates. `time/1` is additionally
|
|
6223
|
+
registered directly in the normal runtime so Trealla-style timing works at the
|
|
6224
|
+
interactive top level without an import; it is absent from the strict ISO
|
|
6225
|
+
registry.
|
|
6224
6226
|
|
|
6225
6227
|
The sources are `src/lib/aggregate.pl`, `src/lib/arithmetic.pl`,
|
|
6226
6228
|
`src/lib/assoc.pl`, `src/lib/atts.pl`, `src/lib/between.pl`,
|
|
@@ -6256,22 +6258,33 @@ between solution branches.
|
|
|
6256
6258
|
|
|
6257
6259
|
<!-- eyeprolog-library-catalog:start -->
|
|
6258
6260
|
|
|
6259
|
-
| Module | Exported predicate indicators |
|
|
6260
|
-
| --- | --- |
|
|
6261
|
-
| `library(aggregate)` | `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
6262
|
-
| `library(
|
|
6263
|
-
| `library(
|
|
6264
|
-
| `library(
|
|
6265
|
-
| `library(
|
|
6266
|
-
| `library(
|
|
6267
|
-
|
|
|
6268
|
-
| `library(
|
|
6269
|
-
| `library(
|
|
6270
|
-
| `library(
|
|
6271
|
-
| `library(
|
|
6272
|
-
| `library(
|
|
6273
|
-
| `library(
|
|
6274
|
-
| `library(
|
|
6261
|
+
| Module | Exported predicate indicators | Primary role |
|
|
6262
|
+
| --- | --- | --- |
|
|
6263
|
+
| `library(aggregate)` | `sumall/3`, `aggregate_min/5`, `aggregate_max/5` | Aggregation |
|
|
6264
|
+
| `library(arithmetic)` | `lsb/2`, `msb/2`, `popcount/2` | CLP(Z) arithmetic support |
|
|
6265
|
+
| `library(assoc)` | `empty_assoc/1`, `assoc_to_list/2`, `get_assoc/3`, `put_assoc/4` | Association lists used by CLP(Z) |
|
|
6266
|
+
| `library(atts)` | `put_atts/2`, `get_atts/2`, `put_attr/3`, `get_attr/3`, `del_attr/2`, `term_attributed_variables/2`, `call_residue_vars/2` | Attributed variables |
|
|
6267
|
+
| `library(between)` | `between/3`, `gen_int/1`, `gen_nat/1`, `numlist/2`, `numlist/3`, `repeat/1` | Integer generation |
|
|
6268
|
+
| `library(clpz)` | `#>/2`, `#</2`, `#>=/2`, `#=</2`, `#=/2`, `#\=/2`, `#\/1`, `#<==>/2`, `#==>/2`, `#<==/2`, `#\//2`, `#\/2`, `#/\/2`, `in/2`, `ins/2`, `all_different/1`, `all_distinct/1`, `nvalue/2`, `sum/3`, `scalar_product/4`, `tuples_in/2`, `labeling/2`, `label/1`, `indomain/1`, `lex_chain/1`, `serialized/2`, `global_cardinality/2`, `global_cardinality/3`, `circuit/1`, `cumulative/1`, `cumulative/2`, `disjoint2/1`, `element/3`, `automaton/3`, `automaton/8`, `zcompare/3`, `chain/2`, `fd_var/1`, `fd_inf/2`, `fd_sup/2`, `fd_size/2`, `fd_dom/2`, `clpz_t/2`, `#=/3`, `#</3` | Constraint logic programming over integers; the final three indicators support reified compatibility libraries |
|
|
6269
|
+
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` | Generic comparison |
|
|
6270
|
+
| `library(dates)` | `difference/3` | ISO duration differences |
|
|
6271
|
+
| `library(dcgs)` | `phrase/2`, `phrase/3`, `seq/3`, `seqq/3` | DCG compatibility; `seq/3` and `seqq/3` are declared as `seq //1` and `seqq //1` |
|
|
6272
|
+
| `library(debug)` | `debug/1`, `debug/3`, `nodebug/1`, `bb_get/2`, `bb_b_put/2` | Scryer compatibility and backtrackable-blackboard support |
|
|
6273
|
+
| `library(error)` | `must_be/2`, `can_be/2`, `instantiation_error/0`, `instantiation_error/1`, `domain_error/2`, `domain_error/3`, `type_error/2`, `type_error/3`, `representation_error/1`, `resource_error/1`, `call_with_error_context/2` | Error checking and construction |
|
|
6274
|
+
| `library(format)` | `format/2` | Formatted output used by bundled libraries |
|
|
6275
|
+
| `library(freeze)` | `freeze/2` | Delayed goals |
|
|
6276
|
+
| `library(iso_ext)` | `call_nth/2`, `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2`, `time/1`, `.../2` | Common ISO extensions |
|
|
6277
|
+
| Normal runtime extension | `dif/2` | Delayed disequality; available without a library import |
|
|
6278
|
+
| `library(lambda)` | `^/3`, `^/4`, `^/5`, `^/6`, `^/7`, `^/8`, `^/9`, `^/10`, `\/1`, `\/2`, `\/3`, `\/4`, `\/5`, `\/6`, `\/7`, `\/8`, `+\/2`, `+\/3`, `+\/4`, `+\/5`, `+\/6`, `+\/7`, `+\/8`, `+\/9` | Higher-order lambda notation |
|
|
6279
|
+
| `library(lists)` | `member/2`, `memberchk/2`, `select/3`, `append/2`, `append/3`, `last/2`, `same_length/2`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `reverse/2`, `length/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `foldl/4`, `foldl/5`, `foldl/6`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` | List relations |
|
|
6280
|
+
| `library(pairs)` | `pairs_keys_values/3`, `pairs_keys/2`, `pairs_values/2`, `group_pairs_by_key/2`, `map_list_to_pairs/3` | Key-value pair support |
|
|
6281
|
+
| `library(primes)` | `smallest_divisor_from/3` | Prime factor support |
|
|
6282
|
+
| `library(prologue)` | `member/2`, `append/3`, `length/2`, `between/3`, `select/3`, `succ/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `call_nth/2`, `freeze/2`, `foldl/4`, `foldl/5`, `foldl/6`, `countall/2` | Legacy facade over canonical focused modules |
|
|
6283
|
+
| `library(random)` | `random/3` | Pure state-threaded random generation |
|
|
6284
|
+
| `library(si)` | `atom_si/1`, `integer_si/1`, `atomic_si/1`, `list_si/1`, `character_si/1`, `term_si/1`, `chars_si/1`, `dif_si/2`, `not_si/1`, `when_si/2` | Sufficient-instantiation checks used by CLP(Z) |
|
|
6285
|
+
| `library(strings)` | `matches/3`, `split/3`, `replace/4`, `lowercase/2`, `uppercase/2`, `trim/2`, `number_string/2`, `atom_string/2`, `term_string/2`, `string_concat/3`, `contains/2`, `matches/2`, `join/3`, `substring/4` | Text relations |
|
|
6286
|
+
| `library(terms)` | `numbervars/3`, `copy_term_nat/2` | Term operations used by bundled libraries |
|
|
6287
|
+
| `library(uuid)` | `uuid/3` | Pure state-threaded UUID generation |
|
|
6275
6288
|
|
|
6276
6289
|
<!-- eyeprolog-library-catalog:end -->
|
|
6277
6290
|
|