eyeprolog 1.2.44 → 1.3.0
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 +31 -2
- package/examples/book/chapter-06/03-square.pl +0 -1
- package/examples/fast-fourier-transform.pl +2 -0
- package/examples/output/fast-fourier-transform.pl +1 -1
- package/examples/sudoku.pl +4 -4
- package/index.d.ts +8 -0
- package/package.json +2 -1
- package/src/cli.js +26 -1
- package/src/index.js +9 -1
- package/src/iso.js +0 -9
- package/src/lib/lists.pl +102 -9
- package/src/program.js +233 -1
- package/src/standard-library.js +56 -5
- package/test/run-interop.mjs +77 -0
- package/test/run-regression.mjs +196 -104
- package/the-art-of-eyeprolog.md +44 -11
package/README.md
CHANGED
|
@@ -123,8 +123,8 @@ noun --> [world] | [prolog].
|
|
|
123
123
|
%% goal: phrase(sentence, Words)
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
EyeProlog also adds
|
|
127
|
-
profile. **
|
|
126
|
+
EyeProlog also adds 102 public library predicate indicators to its 129-entry ISO
|
|
127
|
+
profile. **63 are implemented entirely as ordinary Prolog clauses** in focused
|
|
128
128
|
modules under `src/lib/`; the control predicates and finite-domain
|
|
129
129
|
`library(clpz)` kernel use backtrackable host support.
|
|
130
130
|
They are ISO/IEC 13211-2 modules loaded explicitly by purpose, such as
|
|
@@ -133,6 +133,35 @@ Portable text
|
|
|
133
133
|
predicates use ISO atoms or character lists. The old catch-all
|
|
134
134
|
`library(eyeprolog)` module is no longer needed.
|
|
135
135
|
|
|
136
|
+
## Trealla and Scryer interoperability
|
|
137
|
+
|
|
138
|
+
EyeProlog keeps a conservative source-level interoperability profile separate
|
|
139
|
+
from the larger EyeProlog library surface. `library(lists)` follows the common
|
|
140
|
+
Trealla/Scryer organization for predicates such as `member/2`, `memberchk/2`,
|
|
141
|
+
`append/2-3`, `nth0/3-4`, `nth1/3-4`, `maplist/2-8`, and `foldl/4-6`.
|
|
142
|
+
|
|
143
|
+
Outside `--iso-strict`, an otherwise undefined unqualified call may autoload a
|
|
144
|
+
predicate only when the interop profile has one canonical EyeProlog provider.
|
|
145
|
+
For example, `member/2` autoloads from `library(lists)` and `between/3` can use
|
|
146
|
+
EyeProlog's internal Prologue implementation without requiring portable source
|
|
147
|
+
to name `library(prologue)`. Use `--no-autoload` to disable this convenience.
|
|
148
|
+
Strict ISO mode never autoloads library predicates.
|
|
149
|
+
|
|
150
|
+
Use `-w` / `--warnings` to diagnose dependencies outside the interop profile,
|
|
151
|
+
or `--portable` to make such diagnostics fail the run. This catches both
|
|
152
|
+
implementation-specific library names and EyeProlog-only predicates such as
|
|
153
|
+
`set_nth0/4` even when they live in an otherwise common module.
|
|
154
|
+
|
|
155
|
+
Cross-engine smoke tests live in `test/run-interop.mjs`. With Trealla (`tpl`)
|
|
156
|
+
and Scryer (`scryer-prolog`) installed, run:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
npm run test:interop
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The GitHub interoperability workflow builds both implementations and runs the
|
|
163
|
+
same portable Sudoku source under EyeProlog, Trealla, and Scryer.
|
|
164
|
+
|
|
136
165
|
## Development
|
|
137
166
|
|
|
138
167
|
```sh
|
|
@@ -1 +1 @@
|
|
|
1
|
-
fft([0, 1, 2, 3, 4, 5, 6, 7], [n(
|
|
1
|
+
fft([0, 1, 2, 3, 4, 5, 6, 7], [n(64, op(+, 49, 63)), n(63, op(*, 62, 52)), n(62, w ^ 7), n(61, op(+, 42, 60)), n(60, op(*, 47, 44)), n(59, op(+, 31, 58)), n(58, op(*, 57, 38)), n(57, w ^ 5), n(56, op(+, 11, 55)), n(55, op(*, 24, 21)), n(54, op(+, 49, 53)), n(53, op(*, 50, 52)), n(52, op(+, 34, 51)), n(51, op(*, 47, 36)), n(50, w ^ 3), n(49, op(+, 26, 48)), n(48, op(*, 47, 29)), n(47, w ^ 6), n(46, op(+, 42, 45)), n(45, op(*, 27, 44)), n(44, op(+, 15, 43)), n(43, op(*, 24, 19)), n(42, op(+, 5, 41)), n(41, op(*, 24, 9)), n(40, op(+, 31, 39)), n(39, op(*, 32, 38)), n(38, op(+, 34, 37)), n(37, op(*, 27, 36)), n(36, op(+, 16, 35)), n(35, op(*, 24, 17)), n(34, op(+, 12, 33)), n(33, op(*, 24, 13)), n(32, w ^ 1), n(31, op(+, 26, 30)), n(30, op(*, 27, 29)), n(29, op(+, 6, 28)), n(28, op(*, 24, 7)), n(27, w ^ 2), n(26, op(+, 1, 25)), n(25, op(*, 24, 3)), n(24, w ^ 4), n(23, op(+, 11, 22)), n(22, op(*, 2, 21)), n(21, op(+, 15, 20)), n(20, op(*, 2, 19)), n(19, op(+, 16, 18)), n(18, op(*, 2, 17)), n(17, a(7)), n(16, a(3)), n(15, op(+, 12, 14)), n(14, op(*, 2, 13)), n(13, a(5)), n(12, a(1)), n(11, op(+, 5, 10)), n(10, op(*, 2, 9)), n(9, op(+, 6, 8)), n(8, op(*, 2, 7)), n(7, a(6)), n(6, a(2)), n(5, op(+, 1, 4)), n(4, op(*, 2, 3)), n(3, a(4)), n(2, w ^ 0), n(1, a(0))]).
|
package/examples/sudoku.pl
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
:- use_module(library(prologue), [between/3]).
|
|
2
1
|
:- use_module(library(lists)).
|
|
3
2
|
|
|
4
3
|
% A deterministic 9x9 Sudoku solver.
|
|
@@ -108,6 +107,7 @@ segment_unused(Cells, StartColumn, Digit) :-
|
|
|
108
107
|
Digit \= C.
|
|
109
108
|
|
|
110
109
|
set_cell(Grid, Row, Column, Digit, Result) :-
|
|
111
|
-
nth0(Row, Grid, Cells),
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
nth0(Row, Grid, Cells, OtherRows),
|
|
111
|
+
nth0(Column, Cells, _Old, OtherCells),
|
|
112
|
+
nth0(Column, UpdatedCells, Digit, OtherCells),
|
|
113
|
+
nth0(Row, Result, UpdatedCells, OtherRows).
|
package/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface EyePrologRunOptions {
|
|
|
21
21
|
analyzeNegation?: boolean;
|
|
22
22
|
/** Restrict parsing and execution to ISO/IEC 13211-1:1995 plus Corrigenda 1-3. */
|
|
23
23
|
isoStrict?: boolean;
|
|
24
|
+
/** Autoload uniquely mapped predicates from the conservative interop profile. Defaults to true outside strict ISO mode. */
|
|
25
|
+
autoload?: boolean;
|
|
24
26
|
/** Initial ISO interpretation of double-quoted list notation. Defaults to chars. */
|
|
25
27
|
doubleQuotes?: 'chars' | 'codes' | 'atom';
|
|
26
28
|
ioOptions?: {
|
|
@@ -248,6 +250,9 @@ export const standardLibrarySources: ReadonlyMap<string, { filename: string; sou
|
|
|
248
250
|
export const eyePrologLibraryIndicators: readonly string[];
|
|
249
251
|
export const eyePrologNativeLibraryIndicators: readonly string[];
|
|
250
252
|
export const eyePrologPortableLibraryIndicators: readonly string[];
|
|
253
|
+
export const eyePrologInteropAutoload: Readonly<Record<string, string>>;
|
|
254
|
+
export const eyePrologInteropLibraryIndicators: readonly string[];
|
|
255
|
+
export const eyePrologInteropLibraryModules: readonly string[];
|
|
251
256
|
export class PrologError extends Error {
|
|
252
257
|
formal: string;
|
|
253
258
|
culprit: EyePrologTerm | null;
|
|
@@ -326,6 +331,9 @@ declare const eyeprolog: {
|
|
|
326
331
|
eyePrologLibraryIndicators: typeof eyePrologLibraryIndicators;
|
|
327
332
|
eyePrologNativeLibraryIndicators: typeof eyePrologNativeLibraryIndicators;
|
|
328
333
|
eyePrologPortableLibraryIndicators: typeof eyePrologPortableLibraryIndicators;
|
|
334
|
+
eyePrologInteropAutoload: typeof eyePrologInteropAutoload;
|
|
335
|
+
eyePrologInteropLibraryIndicators: typeof eyePrologInteropLibraryIndicators;
|
|
336
|
+
eyePrologInteropLibraryModules: typeof eyePrologInteropLibraryModules;
|
|
329
337
|
run: typeof run;
|
|
330
338
|
runQuads: typeof runQuads;
|
|
331
339
|
whyProof: typeof whyProof;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.3.0",
|
|
7
7
|
"description": "EyeProlog turns facts and rules into answers and proofs.",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"main": "./index.js",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"test:eyeprolog": "node test/run-all.mjs",
|
|
51
51
|
"test:conformance": "node test/run-conformance.mjs",
|
|
52
52
|
"test:iso-strict": "node test/run-iso-strict.mjs",
|
|
53
|
+
"test:interop": "node test/run-interop.mjs",
|
|
53
54
|
"test:wg17": "node test/run-wg17.mjs",
|
|
54
55
|
"test:examples": "node test/run-examples.mjs",
|
|
55
56
|
"test:regression": "node test/run-regression.mjs",
|
package/src/cli.js
CHANGED
|
@@ -28,6 +28,8 @@ export async function main(argv) {
|
|
|
28
28
|
quads: false,
|
|
29
29
|
stats: false,
|
|
30
30
|
isoStrict: false,
|
|
31
|
+
portable: false,
|
|
32
|
+
autoload: true,
|
|
31
33
|
version: false,
|
|
32
34
|
warnings: false,
|
|
33
35
|
goals: [],
|
|
@@ -51,6 +53,10 @@ export async function main(argv) {
|
|
|
51
53
|
options.stats = true;
|
|
52
54
|
} else if (!endOptions && arg === '--iso-strict') {
|
|
53
55
|
options.isoStrict = true;
|
|
56
|
+
} else if (!endOptions && arg === '--portable') {
|
|
57
|
+
options.portable = true;
|
|
58
|
+
} else if (!endOptions && arg === '--no-autoload') {
|
|
59
|
+
options.autoload = false;
|
|
54
60
|
} else if (!endOptions && (arg === '--version' || arg === '-v')) {
|
|
55
61
|
options.version = true;
|
|
56
62
|
} else if (!endOptions && (arg === '--warnings' || arg === '-w')) {
|
|
@@ -146,9 +152,16 @@ export async function main(argv) {
|
|
|
146
152
|
let program = engine.Program.parseSources(sourceParts, {
|
|
147
153
|
sourceMetadata: options.proof || options.isoStrict,
|
|
148
154
|
isoStrict: options.isoStrict,
|
|
155
|
+
autoload: options.autoload,
|
|
156
|
+
autoloadGoals: options.goals,
|
|
149
157
|
});
|
|
150
158
|
|
|
151
|
-
|
|
159
|
+
const portabilityFailures = program.interopPortabilityWarnings ?? [];
|
|
160
|
+
if (options.warnings || (options.portable && portabilityFailures.length > 0)) printWarnings(program);
|
|
161
|
+
if (options.portable && portabilityFailures.length > 0) {
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
152
165
|
|
|
153
166
|
if (!options.quads || options.goals.length > 0) await runDefault(engine, program, options);
|
|
154
167
|
if (options.quads) {
|
|
@@ -263,6 +276,8 @@ Options:
|
|
|
263
276
|
-s, --stats Print solver and memory statistics to stderr after execution.
|
|
264
277
|
--iso-strict Use ISO/IEC 13211-1 core + Corrigenda 1-3 only;
|
|
265
278
|
reject EyeProlog language extensions and disable automatic tabling.
|
|
279
|
+
--portable Enforce the EyeProlog/Trealla/Scryer interop profile.
|
|
280
|
+
--no-autoload Disable conservative interop predicate autoloading.
|
|
266
281
|
-v, --version Show the package version and exit.
|
|
267
282
|
-w, --warnings Print non-fatal portability warnings to stderr.
|
|
268
283
|
-g, --goal goal Solve goal and print its ground answers; may be repeated.
|
|
@@ -284,6 +299,16 @@ function readStdin() {
|
|
|
284
299
|
}
|
|
285
300
|
|
|
286
301
|
function printWarnings(program) {
|
|
302
|
+
for (const warning of program.interopPortabilityWarnings ?? []) {
|
|
303
|
+
if (warning.kind === 'library') {
|
|
304
|
+
process.stderr.write('eyeprolog warning: non-portable library dependency\n');
|
|
305
|
+
process.stderr.write(` library(${warning.library}) is outside the EyeProlog/Trealla/Scryer interop profile\n`);
|
|
306
|
+
} else if (warning.kind === 'predicate') {
|
|
307
|
+
process.stderr.write('eyeprolog warning: non-portable library predicate\n');
|
|
308
|
+
process.stderr.write(` ${warning.indicator} from library(${warning.library}) is outside the interop profile\n`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
287
312
|
const errors = program.negationStratificationErrors;
|
|
288
313
|
if (errors.length === 0) return;
|
|
289
314
|
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,9 @@ export {
|
|
|
20
20
|
eyePrologLibraryIndicators,
|
|
21
21
|
eyePrologNativeLibraryIndicators,
|
|
22
22
|
eyePrologPortableLibraryIndicators,
|
|
23
|
+
eyePrologInteropAutoload,
|
|
24
|
+
eyePrologInteropLibraryIndicators,
|
|
25
|
+
eyePrologInteropLibraryModules,
|
|
23
26
|
} from './standard-library.js';
|
|
24
27
|
export { StreamManager } from './io.js';
|
|
25
28
|
export { runQuads } from './quads.js';
|
|
@@ -39,7 +42,12 @@ export function run(source, options = {}) {
|
|
|
39
42
|
if (source instanceof Program && requestedStrictIso && source.strictIso !== true) {
|
|
40
43
|
throw new Error('strict ISO mode requires a Program parsed with isoStrict: true');
|
|
41
44
|
}
|
|
42
|
-
const
|
|
45
|
+
const requestedGoals = options.goals ?? (options.goal == null ? [] : [options.goal]);
|
|
46
|
+
const parseOptions = {
|
|
47
|
+
...options,
|
|
48
|
+
sourceMetadata: includeWhy || requestedStrictIso,
|
|
49
|
+
autoloadGoals: requestedGoals,
|
|
50
|
+
};
|
|
43
51
|
let program = source instanceof Program ? source : Program.parse(source, parseOptions);
|
|
44
52
|
const strictIso = requestedStrictIso || program.strictIso === true;
|
|
45
53
|
const runOptions = strictIso
|
package/src/iso.js
CHANGED
|
@@ -1198,15 +1198,6 @@ function scopeReadTerm(term) {
|
|
|
1198
1198
|
|
|
1199
1199
|
function parseReadTermText(text, solver) {
|
|
1200
1200
|
const converted = convertedTermText(text, solver);
|
|
1201
|
-
// A standalone number needs no general term parser or operator tables.
|
|
1202
|
-
// Reuse the same bounded numeric scanner as number_chars/2 so stream reads
|
|
1203
|
-
// and number conversion agree on every accepted numeric token. The stream
|
|
1204
|
-
// scanner has already established that the final full stop is a candidate
|
|
1205
|
-
// terminator; trim only ISO layout immediately before that terminator.
|
|
1206
|
-
const numericText = converted.slice(0, -1).replace(/[\u0009-\u000d\u0020]+$/, '');
|
|
1207
|
-
const numericTerm = parseIsoNumber(numericText);
|
|
1208
|
-
if (numericTerm != null) return numericTerm;
|
|
1209
|
-
|
|
1210
1201
|
const operatorState = createParserOperatorState(solver.program.operators.values(), false);
|
|
1211
1202
|
const clauses = parseClauses(converted, {
|
|
1212
1203
|
sourceMetadata: false,
|
package/src/lib/lists.pl
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
/** List relations, following the library(lists) organization used by Scryer. */
|
|
2
2
|
|
|
3
3
|
:- module(lists, [
|
|
4
|
-
maplist/3,
|
|
5
|
-
append/3,
|
|
6
4
|
member/2,
|
|
5
|
+
memberchk/2,
|
|
7
6
|
select/3,
|
|
7
|
+
append/2,
|
|
8
|
+
append/3,
|
|
8
9
|
last/2,
|
|
10
|
+
same_length/2,
|
|
9
11
|
nth0/3,
|
|
12
|
+
nth0/4,
|
|
10
13
|
nth1/3,
|
|
14
|
+
nth1/4,
|
|
11
15
|
reverse/2,
|
|
12
16
|
length/2,
|
|
17
|
+
maplist/2,
|
|
18
|
+
maplist/3,
|
|
19
|
+
maplist/4,
|
|
20
|
+
maplist/5,
|
|
21
|
+
maplist/6,
|
|
22
|
+
maplist/7,
|
|
23
|
+
maplist/8,
|
|
24
|
+
foldl/4,
|
|
25
|
+
foldl/5,
|
|
26
|
+
foldl/6,
|
|
13
27
|
sum_list/2,
|
|
14
28
|
min_list/2,
|
|
15
29
|
max_list/2,
|
|
@@ -21,43 +35,122 @@
|
|
|
21
35
|
slice/4
|
|
22
36
|
]).
|
|
23
37
|
|
|
38
|
+
:- meta_predicate(maplist(1, '?')).
|
|
24
39
|
:- meta_predicate(maplist(2, '?', '?')).
|
|
40
|
+
:- meta_predicate(maplist(3, '?', '?', '?')).
|
|
41
|
+
:- meta_predicate(maplist(4, '?', '?', '?', '?')).
|
|
42
|
+
:- meta_predicate(maplist(5, '?', '?', '?', '?', '?')).
|
|
43
|
+
:- meta_predicate(maplist(6, '?', '?', '?', '?', '?', '?')).
|
|
44
|
+
:- meta_predicate(maplist(7, '?', '?', '?', '?', '?', '?', '?')).
|
|
45
|
+
:- meta_predicate(foldl(3, '?', '?', '?')).
|
|
46
|
+
:- meta_predicate(foldl(4, '?', '?', '?', '?')).
|
|
47
|
+
:- meta_predicate(foldl(5, '?', '?', '?', '?', '?')).
|
|
25
48
|
:- meta_predicate(countall(0, '?')).
|
|
26
49
|
|
|
27
50
|
% Common pure-Prolog library predicates for EyeProlog.
|
|
28
51
|
%
|
|
29
|
-
%
|
|
30
|
-
%
|
|
31
|
-
%
|
|
52
|
+
% The common Trealla/Scryer-facing surface can be imported explicitly with
|
|
53
|
+
% use_module(library(lists)); EyeProlog may also autoload those common
|
|
54
|
+
% indicators when an otherwise undefined unqualified call is encountered.
|
|
55
|
+
% EyeProlog-specific helpers remain explicit and are diagnosed by --warnings.
|
|
56
|
+
|
|
57
|
+
maplist(_, []).
|
|
58
|
+
maplist(Closure, [A|As]) :-
|
|
59
|
+
call(Closure, A),
|
|
60
|
+
maplist(Closure, As).
|
|
32
61
|
|
|
33
62
|
maplist(_, [], []).
|
|
34
63
|
maplist(Closure, [A|As], [B|Bs]) :-
|
|
35
64
|
call(Closure, A, B),
|
|
36
65
|
maplist(Closure, As, Bs).
|
|
37
66
|
|
|
67
|
+
maplist(_, [], [], []).
|
|
68
|
+
maplist(Closure, [A|As], [B|Bs], [C|Cs]) :-
|
|
69
|
+
call(Closure, A, B, C),
|
|
70
|
+
maplist(Closure, As, Bs, Cs).
|
|
71
|
+
|
|
72
|
+
maplist(_, [], [], [], []).
|
|
73
|
+
maplist(Closure, [A|As], [B|Bs], [C|Cs], [D|Ds]) :-
|
|
74
|
+
call(Closure, A, B, C, D),
|
|
75
|
+
maplist(Closure, As, Bs, Cs, Ds).
|
|
76
|
+
|
|
77
|
+
maplist(_, [], [], [], [], []).
|
|
78
|
+
maplist(Closure, [A|As], [B|Bs], [C|Cs], [D|Ds], [E|Es]) :-
|
|
79
|
+
call(Closure, A, B, C, D, E),
|
|
80
|
+
maplist(Closure, As, Bs, Cs, Ds, Es).
|
|
81
|
+
|
|
82
|
+
maplist(_, [], [], [], [], [], []).
|
|
83
|
+
maplist(Closure, [A|As], [B|Bs], [C|Cs], [D|Ds], [E|Es], [F|Fs]) :-
|
|
84
|
+
call(Closure, A, B, C, D, E, F),
|
|
85
|
+
maplist(Closure, As, Bs, Cs, Ds, Es, Fs).
|
|
86
|
+
|
|
87
|
+
maplist(_, [], [], [], [], [], [], []).
|
|
88
|
+
maplist(Closure, [A|As], [B|Bs], [C|Cs], [D|Ds], [E|Es], [F|Fs], [G|Gs]) :-
|
|
89
|
+
call(Closure, A, B, C, D, E, F, G),
|
|
90
|
+
maplist(Closure, As, Bs, Cs, Ds, Es, Fs, Gs).
|
|
91
|
+
|
|
92
|
+
append([], []).
|
|
93
|
+
append([Xs|Xss], Ys) :-
|
|
94
|
+
append(Xs, Rest, Ys),
|
|
95
|
+
append(Xss, Rest).
|
|
96
|
+
|
|
38
97
|
append([], Ys, Ys).
|
|
39
98
|
append([X|Xs], Ys, [X|Zs]) :- append(Xs, Ys, Zs).
|
|
40
99
|
|
|
41
100
|
member(X, [X|_]).
|
|
42
101
|
member(X, [_|Xs]) :- member(X, Xs).
|
|
43
102
|
|
|
103
|
+
memberchk(X, Xs) :- member(X, Xs), !.
|
|
104
|
+
|
|
44
105
|
select(X, [X|Xs], Xs).
|
|
45
106
|
select(X, [Y|Ys], [Y|Zs]) :- select(X, Ys, Zs).
|
|
46
107
|
|
|
47
108
|
last([X], X).
|
|
48
109
|
last([_|Xs], X) :- last(Xs, X).
|
|
49
110
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
111
|
+
same_length([], []).
|
|
112
|
+
same_length([_|Xs], [_|Ys]) :- same_length(Xs, Ys).
|
|
113
|
+
|
|
114
|
+
nth0(N, List, Elem) :- nth0(N, List, Elem, _).
|
|
115
|
+
|
|
116
|
+
nth0(0, [Elem|Rest], Elem, Rest).
|
|
117
|
+
nth0(N, [X|Xs], Elem, [X|Rest]) :-
|
|
118
|
+
var(N),
|
|
119
|
+
nth0(N0, Xs, Elem, Rest),
|
|
120
|
+
N is N0 + 1.
|
|
121
|
+
nth0(N, [X|Xs], Elem, [X|Rest]) :-
|
|
122
|
+
nonvar(N),
|
|
123
|
+
N > 0,
|
|
124
|
+
N0 is N - 1,
|
|
125
|
+
nth0(N0, Xs, Elem, Rest).
|
|
53
126
|
|
|
54
|
-
nth1(N, List,
|
|
127
|
+
nth1(N, List, Elem) :- nth1(N, List, Elem, _).
|
|
128
|
+
|
|
129
|
+
nth1(N, List, Elem, Rest) :-
|
|
130
|
+
N \== 0,
|
|
131
|
+
nth0(N, [_|List], Elem, [_|Rest]),
|
|
132
|
+
N \== 0.
|
|
55
133
|
|
|
56
134
|
reverse(List, Reversed) :- lists__reverse(List, [], Reversed).
|
|
57
135
|
|
|
58
136
|
length(List, Length) :- nonvar(List), lists__length_count(List, 0, Length).
|
|
59
137
|
length(List, Length) :- var(List), integer(Length), Length >= 0, lists__length_make(Length, List).
|
|
60
138
|
|
|
139
|
+
foldl(_, [], Acc, Acc).
|
|
140
|
+
foldl(Closure, [A|As], Acc0, Acc) :-
|
|
141
|
+
call(Closure, A, Acc0, Acc1),
|
|
142
|
+
foldl(Closure, As, Acc1, Acc).
|
|
143
|
+
|
|
144
|
+
foldl(_, [], [], Acc, Acc).
|
|
145
|
+
foldl(Closure, [A|As], [B|Bs], Acc0, Acc) :-
|
|
146
|
+
call(Closure, A, B, Acc0, Acc1),
|
|
147
|
+
foldl(Closure, As, Bs, Acc1, Acc).
|
|
148
|
+
|
|
149
|
+
foldl(_, [], [], [], Acc, Acc).
|
|
150
|
+
foldl(Closure, [A|As], [B|Bs], [C|Cs], Acc0, Acc) :-
|
|
151
|
+
call(Closure, A, B, C, Acc0, Acc1),
|
|
152
|
+
foldl(Closure, As, Bs, Cs, Acc1, Acc).
|
|
153
|
+
|
|
61
154
|
sum_list(List, Sum) :- lists__sum_list(List, 0, Sum).
|
|
62
155
|
|
|
63
156
|
min_list([X|Xs], Min) :- lists__min_list(Xs, X, Min).
|
package/src/program.js
CHANGED
|
@@ -10,11 +10,17 @@ import {
|
|
|
10
10
|
createParserOperatorState,
|
|
11
11
|
parseClauses,
|
|
12
12
|
parseClausesInto,
|
|
13
|
+
parseGoalText,
|
|
13
14
|
tryParseClausesFastInto,
|
|
14
15
|
} from './parser.js';
|
|
15
16
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
16
17
|
import { currentWorkingDirectory, fs, path } from './platform.js';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
standardLibrarySources,
|
|
20
|
+
eyePrologInteropAutoload,
|
|
21
|
+
eyePrologInteropLibraryIndicators,
|
|
22
|
+
eyePrologInteropLibraryModules,
|
|
23
|
+
} from './standard-library.js';
|
|
18
24
|
import { expandDcgRuleClause } from './dcg.js';
|
|
19
25
|
|
|
20
26
|
const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
|
|
@@ -102,6 +108,9 @@ export class Program {
|
|
|
102
108
|
this.modules = new Map([['user', { name: 'user', exports: new Map(), filename: '<input>' }]]);
|
|
103
109
|
this.moduleImports = new Map();
|
|
104
110
|
this.moduleMetaPredicates = new Map();
|
|
111
|
+
this.autoloadedPredicates = [];
|
|
112
|
+
this.libraryImports = [];
|
|
113
|
+
this.interopPortabilityWarnings = [];
|
|
105
114
|
this.dynamicPredicates = new Set();
|
|
106
115
|
this.strictIso = options.isoStrict === true;
|
|
107
116
|
this.operators = new Map();
|
|
@@ -677,6 +686,19 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
|
|
|
677
686
|
const context = { module: 'user' };
|
|
678
687
|
if (!loadSourceIntoBuilder(builder, text, item.options, ensured, loadedModules, fast, context)) return false;
|
|
679
688
|
}
|
|
689
|
+
const autoloadGoals = parseInteropGoalInputs(options.autoloadGoals, {
|
|
690
|
+
...options,
|
|
691
|
+
operatorState,
|
|
692
|
+
parserFlagState,
|
|
693
|
+
}, builder.program);
|
|
694
|
+
if (options.isoStrict !== true && options.autoload !== false) {
|
|
695
|
+
autoloadInteropDependencies(builder, {
|
|
696
|
+
...options,
|
|
697
|
+
operatorState,
|
|
698
|
+
parserFlagState,
|
|
699
|
+
}, ensured, loadedModules, autoloadGoals);
|
|
700
|
+
}
|
|
701
|
+
analyzeInteropPortability(builder.program, autoloadGoals);
|
|
680
702
|
builder.program.doubleQuotes = parserFlagState.doubleQuotes;
|
|
681
703
|
return true;
|
|
682
704
|
} catch (error) {
|
|
@@ -685,6 +707,208 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
|
|
|
685
707
|
}
|
|
686
708
|
}
|
|
687
709
|
|
|
710
|
+
|
|
711
|
+
const interopIndicatorSet = new Set(eyePrologInteropLibraryIndicators);
|
|
712
|
+
const interopLibraryModuleSet = new Set(eyePrologInteropLibraryModules);
|
|
713
|
+
|
|
714
|
+
function libraryDesignationName(designation) {
|
|
715
|
+
return designation?.type === COMPOUND && designation.name === 'library' && designation.arity === 1 &&
|
|
716
|
+
designation.args[0]?.type === ATOM
|
|
717
|
+
? designation.args[0].name
|
|
718
|
+
: null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function bundledLibraryModule(program, module) {
|
|
722
|
+
return module !== 'user' && program.modules.get(module)?.filename?.startsWith('src/lib/');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function groupDependencies(group) {
|
|
726
|
+
const dependencies = [];
|
|
727
|
+
for (const clause of group.clauses) {
|
|
728
|
+
if (isCompactBinaryClause(clause)) {
|
|
729
|
+
if (clause.bodyName != null) {
|
|
730
|
+
dependencies.push({
|
|
731
|
+
key: `${clause.bodyName}/2`,
|
|
732
|
+
name: clause.bodyName,
|
|
733
|
+
arity: 2,
|
|
734
|
+
module: clause.module ?? group.module,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
for (const goal of clause.body) {
|
|
740
|
+
dependencies.push(...collectGoalDependencies(goal, false, true));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return dependencies;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function parseInteropGoalInputs(inputs, options, program) {
|
|
747
|
+
if (inputs == null) return [];
|
|
748
|
+
const values = Array.isArray(inputs) ? inputs : [inputs];
|
|
749
|
+
return values.filter((value) => value != null).map((value) => {
|
|
750
|
+
if (typeof value !== 'string') return value;
|
|
751
|
+
return parseGoalText(value, {
|
|
752
|
+
doubleQuotes: options.parserFlagState?.doubleQuotes ?? options.doubleQuotes ?? program.doubleQuotes ?? 'chars',
|
|
753
|
+
operatorDefinitions: [...program.operators.values()],
|
|
754
|
+
isoStrict: options.isoStrict === true,
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function extraGoalDependencies(goals) {
|
|
760
|
+
const dependencies = [];
|
|
761
|
+
for (const goal of goals) dependencies.push(...collectGoalDependencies(goal, false, true));
|
|
762
|
+
return dependencies;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function interopAutoloadRequests(program, extraGoals = []) {
|
|
766
|
+
const requests = new Map();
|
|
767
|
+
for (const group of program.groups.values()) {
|
|
768
|
+
if (bundledLibraryModule(program, group.module)) continue;
|
|
769
|
+
for (const dependency of groupDependencies(group)) {
|
|
770
|
+
const targetModule = dependency.module ?? group.module;
|
|
771
|
+
if (program.findGroup(dependency.name, dependency.arity, targetModule)) continue;
|
|
772
|
+
const library = eyePrologInteropAutoload[dependency.key];
|
|
773
|
+
if (library == null) continue;
|
|
774
|
+
const requestKey = `${targetModule}\u0000${dependency.key}`;
|
|
775
|
+
requests.set(requestKey, {
|
|
776
|
+
targetModule,
|
|
777
|
+
library,
|
|
778
|
+
name: dependency.name,
|
|
779
|
+
arity: dependency.arity,
|
|
780
|
+
key: dependency.key,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const goal of program.initializations) {
|
|
785
|
+
for (const dependency of collectGoalDependencies(goal, false, true)) {
|
|
786
|
+
const targetModule = dependency.module ?? 'user';
|
|
787
|
+
if (program.findGroup(dependency.name, dependency.arity, targetModule)) continue;
|
|
788
|
+
const library = eyePrologInteropAutoload[dependency.key];
|
|
789
|
+
if (library == null) continue;
|
|
790
|
+
const requestKey = `${targetModule}\u0000${dependency.key}`;
|
|
791
|
+
requests.set(requestKey, {
|
|
792
|
+
targetModule,
|
|
793
|
+
library,
|
|
794
|
+
name: dependency.name,
|
|
795
|
+
arity: dependency.arity,
|
|
796
|
+
key: dependency.key,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const dependency of extraGoalDependencies(extraGoals)) {
|
|
801
|
+
const targetModule = dependency.module ?? 'user';
|
|
802
|
+
if (program.findGroup(dependency.name, dependency.arity, targetModule)) continue;
|
|
803
|
+
const library = eyePrologInteropAutoload[dependency.key];
|
|
804
|
+
if (library == null) continue;
|
|
805
|
+
const requestKey = `${targetModule}\u0000${dependency.key}`;
|
|
806
|
+
requests.set(requestKey, {
|
|
807
|
+
targetModule,
|
|
808
|
+
library,
|
|
809
|
+
name: dependency.name,
|
|
810
|
+
arity: dependency.arity,
|
|
811
|
+
key: dependency.key,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
return [...requests.values()];
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function autoloadInteropDependencies(builder, options, ensured, loadedModules, extraGoals = []) {
|
|
818
|
+
const program = builder.program;
|
|
819
|
+
while (true) {
|
|
820
|
+
const requests = interopAutoloadRequests(program, extraGoals);
|
|
821
|
+
if (requests.length === 0) return;
|
|
822
|
+
let changed = false;
|
|
823
|
+
for (const request of requests) {
|
|
824
|
+
if (program.findGroup(request.name, request.arity, request.targetModule)) continue;
|
|
825
|
+
if (!loadedModules.has(request.library)) {
|
|
826
|
+
const designation = compound('library', [atom(request.library)]);
|
|
827
|
+
const loaded = readModuleSource(designation, options);
|
|
828
|
+
const childContext = { module: loaded.name };
|
|
829
|
+
// Bundled library sources are small and directive-heavy. Parse them
|
|
830
|
+
// with the general parser even when the user source took the compact
|
|
831
|
+
// fast path; this avoids rebuilding otherwise-fast user programs.
|
|
832
|
+
if (!loadSourceIntoBuilder(
|
|
833
|
+
builder, loaded.text, loaded.options, ensured, loadedModules, false, childContext,
|
|
834
|
+
)) {
|
|
835
|
+
throw new Error(`could not autoload library(${request.library})`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
program.importModule(request.targetModule, request.library, [request]);
|
|
839
|
+
program.autoloadedPredicates.push({
|
|
840
|
+
targetModule: request.targetModule,
|
|
841
|
+
library: request.library,
|
|
842
|
+
indicator: request.key,
|
|
843
|
+
});
|
|
844
|
+
changed = true;
|
|
845
|
+
}
|
|
846
|
+
if (!changed) return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function analyzeInteropPortability(program, extraGoals = []) {
|
|
851
|
+
const warnings = new Map();
|
|
852
|
+
const nonInteropImports = new Map();
|
|
853
|
+
for (const entry of program.libraryImports) {
|
|
854
|
+
if (bundledLibraryModule(program, entry.targetModule)) continue;
|
|
855
|
+
if (!interopLibraryModuleSet.has(entry.library)) {
|
|
856
|
+
warnings.set(`library:${entry.targetModule}:${entry.library}`, {
|
|
857
|
+
kind: 'library',
|
|
858
|
+
library: entry.library,
|
|
859
|
+
targetModule: entry.targetModule,
|
|
860
|
+
});
|
|
861
|
+
const libraries = nonInteropImports.get(entry.targetModule) ?? new Set();
|
|
862
|
+
libraries.add(entry.library);
|
|
863
|
+
nonInteropImports.set(entry.targetModule, libraries);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (entry.imports != null) {
|
|
867
|
+
for (const indicator of entry.imports) {
|
|
868
|
+
if (interopIndicatorSet.has(indicator.key)) continue;
|
|
869
|
+
warnings.set(`predicate:${entry.targetModule}:${entry.library}:${indicator.key}`, {
|
|
870
|
+
kind: 'predicate',
|
|
871
|
+
library: entry.library,
|
|
872
|
+
indicator: indicator.key,
|
|
873
|
+
targetModule: entry.targetModule,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
for (const group of program.groups.values()) {
|
|
880
|
+
if (bundledLibraryModule(program, group.module)) continue;
|
|
881
|
+
for (const dependency of groupDependencies(group)) {
|
|
882
|
+
const targetModule = dependency.module ?? group.module;
|
|
883
|
+
const resolved = program.findGroup(dependency.name, dependency.arity, targetModule);
|
|
884
|
+
if (!resolved || !bundledLibraryModule(program, resolved.module)) continue;
|
|
885
|
+
if (interopIndicatorSet.has(dependency.key)) continue;
|
|
886
|
+
if (nonInteropImports.get(targetModule)?.has(resolved.module)) continue;
|
|
887
|
+
warnings.set(`predicate:${targetModule}:${resolved.module}:${dependency.key}`, {
|
|
888
|
+
kind: 'predicate',
|
|
889
|
+
library: resolved.module,
|
|
890
|
+
indicator: dependency.key,
|
|
891
|
+
targetModule,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
for (const dependency of extraGoalDependencies(extraGoals)) {
|
|
897
|
+
const targetModule = dependency.module ?? 'user';
|
|
898
|
+
const resolved = program.findGroup(dependency.name, dependency.arity, targetModule);
|
|
899
|
+
if (!resolved || !bundledLibraryModule(program, resolved.module)) continue;
|
|
900
|
+
if (interopIndicatorSet.has(dependency.key)) continue;
|
|
901
|
+
if (nonInteropImports.get(targetModule)?.has(resolved.module)) continue;
|
|
902
|
+
warnings.set(`predicate:${targetModule}:${resolved.module}:${dependency.key}`, {
|
|
903
|
+
kind: 'predicate',
|
|
904
|
+
library: resolved.module,
|
|
905
|
+
indicator: dependency.key,
|
|
906
|
+
targetModule,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
program.interopPortabilityWarnings = [...warnings.values()];
|
|
910
|
+
}
|
|
911
|
+
|
|
688
912
|
function sourceOptionsFor(source, options) {
|
|
689
913
|
if (typeof source === 'string') return options;
|
|
690
914
|
return {
|
|
@@ -736,6 +960,14 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
736
960
|
flush();
|
|
737
961
|
clause.module = context.module;
|
|
738
962
|
builder.addClauses([clause]);
|
|
963
|
+
const libraryName = libraryDesignationName(use.designation);
|
|
964
|
+
if (libraryName != null) {
|
|
965
|
+
builder.program.libraryImports.push({
|
|
966
|
+
targetModule: context.module,
|
|
967
|
+
library: libraryName,
|
|
968
|
+
imports: use.imports == null ? null : use.imports.map((indicator) => ({ ...indicator })),
|
|
969
|
+
});
|
|
970
|
+
}
|
|
739
971
|
const loaded = readModuleSource(use.designation, options);
|
|
740
972
|
if (!loadedModules.has(loaded.name)) {
|
|
741
973
|
const childContext = { module: loaded.name };
|
package/src/standard-library.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ISO/IEC 13211-2 module sources shipped with EyeProlog.
|
|
2
|
-
// The sources are registered here so library(Name) works in Node and browsers
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// The sources are registered here so library(Name) works in Node and browsers.
|
|
3
|
+
// Modules are loaded on demand by explicit use_module/1-2 or by the conservative
|
|
4
|
+
// source-level interop autoloader declared below.
|
|
5
5
|
import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
6
6
|
import { clpzBuiltins } from './clpz.js';
|
|
7
7
|
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
@@ -62,8 +62,8 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
|
62
62
|
'maplist/6', 'maplist/7', 'maplist/8', 'between/3',
|
|
63
63
|
'smallest_divisor_from/3', 'random/3', 'matches/3', 'split/3',
|
|
64
64
|
'replace/4', 'lowercase/2', 'uppercase/2', 'trim/2', 'number_string/2',
|
|
65
|
-
'atom_string/2', 'term_string/2', 'append/3', 'string_concat/3', 'contains/2',
|
|
66
|
-
'matches/2', 'join/3', 'substring/4', 'member/2', 'select/3', 'last/2',
|
|
65
|
+
'atom_string/2', 'term_string/2', 'append/2', 'append/3', 'string_concat/3', 'contains/2',
|
|
66
|
+
'matches/2', 'join/3', 'substring/4', 'member/2', 'memberchk/2', 'select/3', 'last/2', 'same_length/2',
|
|
67
67
|
'nth0/3', 'nth0/4', 'nth1/3', 'nth1/4', 'set_nth0/4', 'take/3', 'drop/3', 'slice/4', 'reverse/2',
|
|
68
68
|
'length/2', 'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
|
|
69
69
|
'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
|
|
@@ -74,6 +74,57 @@ export const eyePrologLibraryIndicators = Object.freeze([
|
|
|
74
74
|
...eyePrologNativeLibraryIndicators,
|
|
75
75
|
]);
|
|
76
76
|
|
|
77
|
+
|
|
78
|
+
// Conservative cross-implementation profile. These predicate indicators are
|
|
79
|
+
// intentionally limited to interfaces that EyeProlog wants to keep source-
|
|
80
|
+
// compatible with the Trealla/Scryer ecosystem. The value is the canonical
|
|
81
|
+
// EyeProlog library used when an otherwise undefined call is autoloaded.
|
|
82
|
+
//
|
|
83
|
+
// This is deliberately separate from eyePrologPortableLibraryIndicators:
|
|
84
|
+
// "portable" there means implemented in Prolog rather than as a host builtin;
|
|
85
|
+
// this table means interoperable source-level API.
|
|
86
|
+
export const eyePrologInteropAutoload = Object.freeze({
|
|
87
|
+
'member/2': 'lists',
|
|
88
|
+
'memberchk/2': 'lists',
|
|
89
|
+
'select/3': 'lists',
|
|
90
|
+
'append/2': 'lists',
|
|
91
|
+
'append/3': 'lists',
|
|
92
|
+
'last/2': 'lists',
|
|
93
|
+
'same_length/2': 'lists',
|
|
94
|
+
'nth0/3': 'lists',
|
|
95
|
+
'nth0/4': 'lists',
|
|
96
|
+
'nth1/3': 'lists',
|
|
97
|
+
'nth1/4': 'lists',
|
|
98
|
+
'reverse/2': 'lists',
|
|
99
|
+
'length/2': 'lists',
|
|
100
|
+
'maplist/2': 'lists',
|
|
101
|
+
'maplist/3': 'lists',
|
|
102
|
+
'maplist/4': 'lists',
|
|
103
|
+
'maplist/5': 'lists',
|
|
104
|
+
'maplist/6': 'lists',
|
|
105
|
+
'maplist/7': 'lists',
|
|
106
|
+
'maplist/8': 'lists',
|
|
107
|
+
'foldl/4': 'lists',
|
|
108
|
+
'foldl/5': 'lists',
|
|
109
|
+
'foldl/6': 'lists',
|
|
110
|
+
'sum_list/2': 'lists',
|
|
111
|
+
'list_to_set/2': 'lists',
|
|
112
|
+
// Trealla and Scryer expose between/3 without an EyeProlog-style
|
|
113
|
+
// library(prologue) dependency. EyeProlog keeps its implementation in the
|
|
114
|
+
// Prologue module but autoloads it so portable source need not name that
|
|
115
|
+
// implementation-specific library.
|
|
116
|
+
'between/3': 'prologue',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const eyePrologInteropLibraryIndicators = Object.freeze(
|
|
120
|
+
Object.keys(eyePrologInteropAutoload),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Libraries whose *name* is part of the current interop profile. A program
|
|
124
|
+
// may freely use_module(library(lists)); predicates in that module which are
|
|
125
|
+
// outside eyePrologInteropLibraryIndicators are still diagnosed when used.
|
|
126
|
+
export const eyePrologInteropLibraryModules = Object.freeze(['lists']);
|
|
127
|
+
|
|
77
128
|
function runtimeStatistics(solver) {
|
|
78
129
|
return { ...solver.stats, ...memoryStatistics() };
|
|
79
130
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cross-implementation smoke tests for source-level Prolog interoperability.
|
|
3
|
+
//
|
|
4
|
+
// By default this runner requires Trealla (`tpl`) and Scryer (`scryer-prolog`)
|
|
5
|
+
// to be installed. Pass --allow-missing for local development when only the
|
|
6
|
+
// EyeProlog leg is available.
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const allowMissing = process.argv.includes('--allow-missing');
|
|
14
|
+
const sudoku = path.join(root, 'examples', 'sudoku.pl');
|
|
15
|
+
const goal = 'sudoku_solution(S), write(S), nl, fail; halt';
|
|
16
|
+
const expected = '[[5,3,4,6,7,8,9,1,2],[6,7,2,1,9,5,3,4,8],[1,9,8,3,4,2,5,6,7],[8,5,9,7,6,1,4,2,3],[4,2,6,8,5,3,7,9,1],[7,1,3,9,2,4,8,5,6],[9,6,1,5,3,7,2,8,4],[2,8,7,4,1,9,6,3,5],[3,4,5,2,8,6,1,7,9]]';
|
|
17
|
+
|
|
18
|
+
const engines = [
|
|
19
|
+
{
|
|
20
|
+
name: 'EyeProlog',
|
|
21
|
+
command: process.execPath,
|
|
22
|
+
args: [path.join(root, 'bin', 'eyeprolog.js'), '--portable', '-g', goal, sudoku],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Trealla',
|
|
26
|
+
command: process.env.TPL ?? 'tpl',
|
|
27
|
+
args: ['-q', '-g', goal, sudoku],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Scryer',
|
|
31
|
+
command: process.env.SCRYER_PROLOG ?? 'scryer-prolog',
|
|
32
|
+
args: ['-f', '-g', goal, sudoku],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
let failed = false;
|
|
37
|
+
let executed = 0;
|
|
38
|
+
for (const engine of engines) {
|
|
39
|
+
const result = spawnSync(engine.command, engine.args, {
|
|
40
|
+
cwd: root,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
timeout: 120000,
|
|
43
|
+
});
|
|
44
|
+
if (result.error?.code === 'ENOENT') {
|
|
45
|
+
if (allowMissing && engine.name !== 'EyeProlog') {
|
|
46
|
+
console.log(`SKIP ${engine.name}: ${engine.command} not installed`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
console.error(`FAIL ${engine.name}: ${engine.command} not installed`);
|
|
50
|
+
failed = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
executed++;
|
|
54
|
+
if (result.error) {
|
|
55
|
+
console.error(`FAIL ${engine.name}: ${result.error.message}`);
|
|
56
|
+
failed = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
console.error(`FAIL ${engine.name}: exit ${result.status}`);
|
|
61
|
+
if (result.stderr) console.error(result.stderr.trim());
|
|
62
|
+
failed = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const lines = String(result.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
66
|
+
if (!lines.includes(expected)) {
|
|
67
|
+
console.error(`FAIL ${engine.name}: Sudoku output mismatch`);
|
|
68
|
+
console.error(`stdout: ${JSON.stringify(result.stdout)}`);
|
|
69
|
+
if (result.stderr) console.error(`stderr: ${result.stderr.trim()}`);
|
|
70
|
+
failed = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
console.log(`OK ${engine.name}: portable Sudoku`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (failed) process.exitCode = 1;
|
|
77
|
+
else console.log(`OK ${executed}/${allowMissing ? engines.length : executed} available interop engines passed`);
|
package/test/run-regression.mjs
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
eyePrologLibraryIndicators,
|
|
24
24
|
eyePrologNativeLibraryIndicators,
|
|
25
25
|
eyePrologPortableLibraryIndicators,
|
|
26
|
+
eyePrologInteropAutoload,
|
|
27
|
+
eyePrologInteropLibraryIndicators,
|
|
28
|
+
eyePrologInteropLibraryModules,
|
|
26
29
|
atom,
|
|
27
30
|
compound,
|
|
28
31
|
listFromItems,
|
|
@@ -692,108 +695,6 @@ c4 ?- call((!;1)).
|
|
|
692
695
|
}
|
|
693
696
|
},
|
|
694
697
|
},
|
|
695
|
-
{
|
|
696
|
-
name: 'read/2 and number_chars/2 agree on bounded numeric syntax (issue #29)',
|
|
697
|
-
run: () => {
|
|
698
|
-
const registry = createDefaultRegistry();
|
|
699
|
-
const numberChars = registry.get('number_chars', 2).handler;
|
|
700
|
-
const read = registry.get('read', 2).handler;
|
|
701
|
-
const solver = new Solver(Program.parse(''), { registry, ioOptions: { input: '' } });
|
|
702
|
-
const stream = solver.io.resolve(0);
|
|
703
|
-
const streamTerm = compound('$stream', [numberTerm('0')]);
|
|
704
|
-
const alphabet = ['0', '1', '2', '7', '8', '9', 'a', 'f', 'x', 'e', 'E', '+', '-', '.', "'", '\\', ' '];
|
|
705
|
-
let checked = 0;
|
|
706
|
-
let accepted = 0;
|
|
707
|
-
|
|
708
|
-
const visit = (prefix, remaining) => {
|
|
709
|
-
if (remaining === 0) {
|
|
710
|
-
checked++;
|
|
711
|
-
const converted = variable('Converted');
|
|
712
|
-
const conversionGoal = compound('number_chars', [
|
|
713
|
-
converted,
|
|
714
|
-
listFromItems(Array.from(prefix, atom)),
|
|
715
|
-
]);
|
|
716
|
-
let conversion;
|
|
717
|
-
try {
|
|
718
|
-
conversion = numberChars({ goal: conversionGoal, env: new Env() }).next();
|
|
719
|
-
} catch (_) {
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
if (conversion.done) return;
|
|
723
|
-
accepted++;
|
|
724
|
-
const expected = copyResolved(converted, conversion.value);
|
|
725
|
-
|
|
726
|
-
stream.content = `${prefix}. `;
|
|
727
|
-
stream.position = 0;
|
|
728
|
-
stream.pastEnd = false;
|
|
729
|
-
const readValue = variable('ReadValue');
|
|
730
|
-
const readGoal = compound('read', [streamTerm, readValue]);
|
|
731
|
-
const answer = read({ solver, goal: readGoal, env: new Env() }).next();
|
|
732
|
-
if (answer.done) throw new Error(`read/2 rejected number_chars/2 spelling ${JSON.stringify(prefix)}`);
|
|
733
|
-
const actual = copyResolved(readValue, answer.value);
|
|
734
|
-
assertEqual(actual.type, 'number', `read/2 type for ${JSON.stringify(prefix)}`);
|
|
735
|
-
assertEqual(actual.name, expected.name, `read/2 value for ${JSON.stringify(prefix)}`);
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
for (const character of alphabet) visit(prefix + character, remaining - 1);
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
for (let length = 1; length <= 4; length++) visit('', length);
|
|
742
|
-
assertEqual(checked, 88740, 'bounded numeric spellings checked');
|
|
743
|
-
if (accepted < 1000) throw new Error(`unexpectedly small accepted numeric corpus: ${accepted}`);
|
|
744
|
-
},
|
|
745
|
-
},
|
|
746
|
-
{
|
|
747
|
-
name: 'number_chars/2 to read/2 cross-check stays bounded under a small heap (issue #29)',
|
|
748
|
-
run: () => {
|
|
749
|
-
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
750
|
-
const script = `
|
|
751
|
-
import {
|
|
752
|
-
Program, Solver, Env, atom, compound, variable, listFromItems,
|
|
753
|
-
numberTerm, copyResolved, createDefaultRegistry,
|
|
754
|
-
} from ${JSON.stringify(engineUrl)};
|
|
755
|
-
const registry = createDefaultRegistry();
|
|
756
|
-
const numberChars = registry.get('number_chars', 2).handler;
|
|
757
|
-
const read = registry.get('read', 2).handler;
|
|
758
|
-
const solver = new Solver(Program.parse(''), { registry, ioOptions: { input: '' } });
|
|
759
|
-
const stream = solver.io.resolve(0);
|
|
760
|
-
const streamTerm = compound('$stream', [numberTerm('0')]);
|
|
761
|
-
for (let i = 0; i < 250000; i++) {
|
|
762
|
-
const text = String(i);
|
|
763
|
-
const converted = variable('Converted');
|
|
764
|
-
const conversionGoal = compound('number_chars', [
|
|
765
|
-
converted,
|
|
766
|
-
listFromItems(Array.from(text, atom)),
|
|
767
|
-
]);
|
|
768
|
-
const conversion = numberChars({ goal: conversionGoal, env: new Env() }).next();
|
|
769
|
-
if (conversion.done) throw new Error('number_chars/2 failed at ' + i);
|
|
770
|
-
const expected = copyResolved(converted, conversion.value);
|
|
771
|
-
|
|
772
|
-
stream.content = text + '. ';
|
|
773
|
-
stream.position = 0;
|
|
774
|
-
stream.pastEnd = false;
|
|
775
|
-
const readValue = variable('ReadValue');
|
|
776
|
-
const readGoal = compound('read', [streamTerm, readValue]);
|
|
777
|
-
const answer = read({ solver, goal: readGoal, env: new Env() }).next();
|
|
778
|
-
if (answer.done) throw new Error('read/2 failed at ' + i);
|
|
779
|
-
const actual = copyResolved(readValue, answer.value);
|
|
780
|
-
if (actual.type !== 'number' || actual.name !== expected.name) {
|
|
781
|
-
throw new Error('number mismatch at ' + i + ': ' + actual.name + ' != ' + expected.name);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
process.stdout.write('250000');
|
|
785
|
-
`;
|
|
786
|
-
const result = spawnSync(process.execPath, [
|
|
787
|
-
'--max-old-space-size=32',
|
|
788
|
-
'--input-type=module',
|
|
789
|
-
'--eval',
|
|
790
|
-
script,
|
|
791
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
792
|
-
if (result.error) throw result.error;
|
|
793
|
-
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
794
|
-
assertEqual(result.stdout, '250000', 'number_chars/read cross-check count');
|
|
795
|
-
},
|
|
796
|
-
},
|
|
797
698
|
{
|
|
798
699
|
name: 'number syntax and number_chars normalize floating-point negative zero',
|
|
799
700
|
run: () => {
|
|
@@ -1270,6 +1171,8 @@ c4 ?- call((!;1)).
|
|
|
1270
1171
|
assertIncludes(result.stdout, '-p, --proof', 'stdout');
|
|
1271
1172
|
assertIncludes(result.stdout, '-q, --quads', 'stdout');
|
|
1272
1173
|
assertIncludes(result.stdout, '-s, --stats', 'stdout');
|
|
1174
|
+
assertIncludes(result.stdout, '--portable', 'stdout');
|
|
1175
|
+
assertIncludes(result.stdout, '--no-autoload', 'stdout');
|
|
1273
1176
|
assertIncludes(result.stdout, '-v, --version', 'stdout');
|
|
1274
1177
|
assertIncludes(result.stdout, '-w, --warnings', 'stdout');
|
|
1275
1178
|
assertIncludes(result.stdout, '-v, --version Show the package version and exit.\n -w, --warnings Print non-fatal portability warnings to stderr.', 'stdout');
|
|
@@ -2107,6 +2010,125 @@ c4 ?- call((!;1)).
|
|
|
2107
2010
|
assertIncludes(result.stderr, 'existence_error(procedure)', 'stderr');
|
|
2108
2011
|
},
|
|
2109
2012
|
},
|
|
2013
|
+
{
|
|
2014
|
+
name: 'portable library predicates autoload without use_module directives',
|
|
2015
|
+
run: () => {
|
|
2016
|
+
const input = '%% goal: answer(X)\nanswer(X) :- member(X, [a,b]).\n';
|
|
2017
|
+
const result = runCli(['-'], { input });
|
|
2018
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2019
|
+
assertEqual(result.stdout, 'answer(a).\nanswer(b).\n', 'stdout');
|
|
2020
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2021
|
+
},
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
name: 'between/3 autoload removes the EyeProlog-specific prologue dependency',
|
|
2025
|
+
run: () => {
|
|
2026
|
+
const input = '%% goal: answer(X)\nanswer(X) :- between(1, 3, X).\n';
|
|
2027
|
+
const result = runCli(['-'], { input });
|
|
2028
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2029
|
+
assertEqual(result.stdout, 'answer(1).\nanswer(2).\nanswer(3).\n', 'stdout');
|
|
2030
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2031
|
+
},
|
|
2032
|
+
},
|
|
2033
|
+
{
|
|
2034
|
+
name: 'strict ISO mode disables interop autoloading',
|
|
2035
|
+
run: () => {
|
|
2036
|
+
const input = '%% goal: answer(X)\nanswer(X) :- member(X, [a,b]).\n';
|
|
2037
|
+
const result = runCli(['--iso-strict', '-'], { input });
|
|
2038
|
+
assertEqual(result.status, 1, 'exit status');
|
|
2039
|
+
assertIncludes(result.stderr, 'existence_error(procedure)', 'stderr');
|
|
2040
|
+
assertIncludes(result.stderr, '/(member, 2)', 'stderr');
|
|
2041
|
+
},
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
name: 'CLI top-level goals participate in interop autoloading',
|
|
2045
|
+
run: () => {
|
|
2046
|
+
const result = runCli(['-g', 'member(X,[a,b])', '-'], { input: '' });
|
|
2047
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2048
|
+
assertEqual(result.stdout, 'member(a, "ab").\nmember(b, "ab").\n', 'stdout');
|
|
2049
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2050
|
+
},
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
name: '--no-autoload also applies to CLI top-level goals',
|
|
2054
|
+
run: () => {
|
|
2055
|
+
const result = runCli(['--no-autoload', '-g', 'member(X,[a])', '-'], { input: '' });
|
|
2056
|
+
assertEqual(result.status, 1, 'exit status');
|
|
2057
|
+
assertIncludes(result.stderr, 'existence_error(procedure)', 'stderr');
|
|
2058
|
+
assertIncludes(result.stderr, '/(member, 2)', 'stderr');
|
|
2059
|
+
},
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
name: '--portable checks non-portable predicates used only by top-level goals',
|
|
2063
|
+
run: () => {
|
|
2064
|
+
const input = ':- use_module(library(lists)).\n';
|
|
2065
|
+
const result = runCli(['--portable', '-g', 'set_nth0(0,[a],b,X)', '-'], { input });
|
|
2066
|
+
assertEqual(result.status, 1, 'exit status');
|
|
2067
|
+
assertEqual(result.stdout, '', 'stdout');
|
|
2068
|
+
assertIncludes(result.stderr, 'non-portable library predicate', 'stderr');
|
|
2069
|
+
assertIncludes(result.stderr, 'set_nth0/4', 'stderr');
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
name: '--portable accepts the common interop profile',
|
|
2074
|
+
run: () => {
|
|
2075
|
+
const input = ':- use_module(library(lists)).\n%% goal: answer(X)\nanswer(X) :- member(X, [a]).\n';
|
|
2076
|
+
const result = runCli(['--portable', '-'], { input });
|
|
2077
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2078
|
+
assertEqual(result.stdout, 'answer(a).\n', 'stdout');
|
|
2079
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2080
|
+
},
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
name: '--portable rejects implementation-specific library dependencies',
|
|
2084
|
+
run: () => {
|
|
2085
|
+
const input = ':- use_module(library(prologue), [between/3]).\n%% goal: answer\nanswer :- between(1, 1, _).\n';
|
|
2086
|
+
const result = runCli(['--portable', '-'], { input });
|
|
2087
|
+
assertEqual(result.status, 1, 'exit status');
|
|
2088
|
+
assertEqual(result.stdout, '', 'stdout');
|
|
2089
|
+
assertIncludes(result.stderr, 'non-portable library dependency', 'stderr');
|
|
2090
|
+
},
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
name: '--no-autoload exposes unresolved portable dependencies',
|
|
2094
|
+
run: () => {
|
|
2095
|
+
const input = '%% goal: answer(X)\nanswer(X) :- member(X, [a]).\n';
|
|
2096
|
+
const result = runCli(['--no-autoload', '-'], { input });
|
|
2097
|
+
assertEqual(result.status, 1, 'exit status');
|
|
2098
|
+
assertIncludes(result.stderr, 'existence_error(procedure)', 'stderr');
|
|
2099
|
+
assertIncludes(result.stderr, '/(member, 2)', 'stderr');
|
|
2100
|
+
},
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
name: '--warnings flags explicit library(prologue) dependencies',
|
|
2104
|
+
run: () => {
|
|
2105
|
+
const input = ':- use_module(library(prologue), [between/3]).\n%% goal: answer\nanswer :- between(1, 1, _).\n';
|
|
2106
|
+
const result = runCli(['--warnings', '-'], { input });
|
|
2107
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2108
|
+
assertIncludes(result.stderr, 'eyeprolog warning: non-portable library dependency\n', 'stderr');
|
|
2109
|
+
assertIncludes(result.stderr, 'library(prologue) is outside the EyeProlog/Trealla/Scryer interop profile', 'stderr');
|
|
2110
|
+
},
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
name: '--warnings flags EyeProlog-only predicates from library(lists)',
|
|
2114
|
+
run: () => {
|
|
2115
|
+
const input = ':- use_module(library(lists)).\n%% goal: answer(X)\nanswer(X) :- set_nth0(0, [a], b, X).\n';
|
|
2116
|
+
const result = runCli(['--warnings', '-'], { input });
|
|
2117
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2118
|
+
assertIncludes(result.stderr, 'eyeprolog warning: non-portable library predicate\n', 'stderr');
|
|
2119
|
+
assertIncludes(result.stderr, 'set_nth0/4 from library(lists) is outside the interop profile', 'stderr');
|
|
2120
|
+
},
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
name: '--warnings stays quiet for the common library(lists) profile',
|
|
2124
|
+
run: () => {
|
|
2125
|
+
const input = ':- use_module(library(lists)).\n%% goal: answer(X)\nanswer(X) :- member(X, [a]).\n';
|
|
2126
|
+
const result = runCli(['--warnings', '-'], { input });
|
|
2127
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2128
|
+
assertEqual(result.stdout, 'answer(a).\n', 'stdout');
|
|
2129
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2110
2132
|
{
|
|
2111
2133
|
name: '--warnings prints unstratified negation diagnostics without failing',
|
|
2112
2134
|
run: () => {
|
|
@@ -3055,6 +3077,71 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
3055
3077
|
assertEqual(answers.join('\n'), 'answer("ab")', 'answers');
|
|
3056
3078
|
},
|
|
3057
3079
|
},
|
|
3080
|
+
{
|
|
3081
|
+
name: 'library(lists) exposes the Trealla/Scryer common surface',
|
|
3082
|
+
run: () => {
|
|
3083
|
+
const program = Program.parse(`:- use_module(library(lists)).
|
|
3084
|
+
identity(X, X).
|
|
3085
|
+
pair(A, B, A-B).
|
|
3086
|
+
step(X, A0, A) :- A is A0 + X.
|
|
3087
|
+
check(A, B, C, D, E, F) :-
|
|
3088
|
+
append([[a],[b]], A),
|
|
3089
|
+
memberchk(a, A),
|
|
3090
|
+
same_length(A, [_,_]),
|
|
3091
|
+
nth0(1, A, b, B),
|
|
3092
|
+
maplist(identity, A, C),
|
|
3093
|
+
maplist(pair, A, [1,2], D),
|
|
3094
|
+
foldl(step, [1,2,3], 0, E),
|
|
3095
|
+
nth1(2, A, b, F).
|
|
3096
|
+
`);
|
|
3097
|
+
assertEqual(program.findGroup('append', 2)?.module, 'lists', 'append/2 import');
|
|
3098
|
+
assertEqual(program.findGroup('memberchk', 2)?.module, 'lists', 'memberchk/2 import');
|
|
3099
|
+
assertEqual(program.findGroup('same_length', 2)?.module, 'lists', 'same_length/2 import');
|
|
3100
|
+
assertEqual(program.findGroup('nth0', 4)?.module, 'lists', 'nth0/4 import');
|
|
3101
|
+
assertEqual(program.findGroup('maplist', 2)?.module, 'lists', 'maplist/2 import');
|
|
3102
|
+
assertEqual(program.findGroup('maplist', 3)?.module, 'lists', 'maplist/3 import');
|
|
3103
|
+
assertEqual(program.findGroup('foldl', 4)?.module, 'lists', 'foldl/4 import');
|
|
3104
|
+
const result = run(program, { goal: 'check(A,B,C,D,E,F)' });
|
|
3105
|
+
assertIncludes(result.stdout, 'check("ab", "a", "ab", [a - 1, b - 2], 6, "a").\n', 'stdout');
|
|
3106
|
+
},
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
name: 'autoload metadata records canonical interop imports',
|
|
3110
|
+
run: () => {
|
|
3111
|
+
const program = Program.parse('answer(X) :- member(X, [a]), between(1, 1, _).\n');
|
|
3112
|
+
assertEqual(program.autoloadedPredicates.length, 2, 'autoloaded predicate count');
|
|
3113
|
+
assertEqual(program.autoloadedPredicates.map((entry) => `${entry.indicator}:${entry.library}`).sort().join(','),
|
|
3114
|
+
'between/3:prologue,member/2:lists', 'autoload mapping');
|
|
3115
|
+
assertEqual(program.interopPortabilityWarnings.length, 0, 'autoloaded portable calls are warning-free');
|
|
3116
|
+
},
|
|
3117
|
+
},
|
|
3118
|
+
{
|
|
3119
|
+
name: 'JavaScript run top-level goals participate in interop autoloading',
|
|
3120
|
+
run: () => {
|
|
3121
|
+
const result = runEyeProlog('', { goal: 'member(X,[a,b])' });
|
|
3122
|
+
assertEqual(result.stdout, 'member(a, "ab").\nmember(b, "ab").\n', 'stdout');
|
|
3123
|
+
},
|
|
3124
|
+
},
|
|
3125
|
+
{
|
|
3126
|
+
name: 'JavaScript run can disable top-level goal autoloading',
|
|
3127
|
+
run: () => {
|
|
3128
|
+
let error = null;
|
|
3129
|
+
try {
|
|
3130
|
+
runEyeProlog('', { goal: 'member(X,[a])', autoload: false });
|
|
3131
|
+
} catch (caught) {
|
|
3132
|
+
error = caught;
|
|
3133
|
+
}
|
|
3134
|
+
assertIncludes(error?.message ?? '', 'existence_error(procedure)', 'error');
|
|
3135
|
+
},
|
|
3136
|
+
},
|
|
3137
|
+
{
|
|
3138
|
+
name: 'autoload can be disabled explicitly in the JavaScript API',
|
|
3139
|
+
run: () => {
|
|
3140
|
+
const program = Program.parse('answer(X) :- member(X, [a]).\n', { autoload: false });
|
|
3141
|
+
assertEqual(program.findGroup('member', 2), null, 'member/2 remains unresolved');
|
|
3142
|
+
assertEqual(program.autoloadedPredicates.length, 0, 'no autoloads');
|
|
3143
|
+
},
|
|
3144
|
+
},
|
|
3058
3145
|
{
|
|
3059
3146
|
name: 'program and solver public classes',
|
|
3060
3147
|
run: () => {
|
|
@@ -3345,10 +3432,15 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
3345
3432
|
assertEqual(Boolean(library.get('statistics', 0)), true, 'statistics/0 is an EyeProlog observability extension');
|
|
3346
3433
|
assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
|
|
3347
3434
|
assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
|
|
3348
|
-
assertEqual(eyePrologPortableLibraryIndicators.length,
|
|
3435
|
+
assertEqual(eyePrologPortableLibraryIndicators.length, 62, 'portable Prolog library count');
|
|
3436
|
+
assertEqual(eyePrologInteropLibraryIndicators.length, 26, 'cross-implementation interop profile count');
|
|
3437
|
+
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists', 'common explicit library module profile');
|
|
3438
|
+
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
3439
|
+
assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
|
|
3440
|
+
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
3349
3441
|
assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
|
|
3350
3442
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
|
3351
|
-
assertEqual(eyePrologLibraryIndicators.length,
|
|
3443
|
+
assertEqual(eyePrologLibraryIndicators.length, 102, 'complete EyeProlog library surface');
|
|
3352
3444
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
3353
3445
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
3354
3446
|
assertEqual(library.get('eyeprolog__call_nth', 2)?.eyePrologLibrary, true, 'private adapter is marked as library support');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1065,7 +1065,6 @@ safe_reading(Sensor, Value) :-
|
|
|
1065
1065
|
already-bound value:
|
|
1066
1066
|
|
|
1067
1067
|
```eyeprolog
|
|
1068
|
-
:- use_module(library(prologue), [between/3]).
|
|
1069
1068
|
:- use_module(library(lists)).
|
|
1070
1069
|
|
|
1071
1070
|
square(N, Square) :-
|
|
@@ -5975,8 +5974,8 @@ so side effects occur in Prolog execution order.
|
|
|
5975
5974
|
|
|
5976
5975
|
### The EyeProlog library
|
|
5977
5976
|
|
|
5978
|
-
EyeProlog exposes **
|
|
5979
|
-
indicators in its isolated ISO profile. **
|
|
5977
|
+
EyeProlog exposes **102 library predicate indicators** in addition to the 129
|
|
5978
|
+
indicators in its isolated ISO profile. **63 are defined as ordinary Prolog
|
|
5980
5979
|
clauses** in focused modules under `src/lib/`. The remaining 39
|
|
5981
5980
|
are public wrappers around backtrackable host support: Prologue `call_nth/2` and
|
|
5982
5981
|
`freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
|
|
@@ -5994,9 +5993,10 @@ only the modules it needs, and
|
|
|
5994
5993
|
module implements p.p.1 through p.p.11 of the
|
|
5995
5994
|
[working-draft Prologue](https://www.complang.tuwien.ac.at/ulrich/iso-prolog/prologue),
|
|
5996
5995
|
with the published Prologue, `call_nth/2`, and `length/2` quads retained as
|
|
5997
|
-
offline regressions. `src/standard-library.js` registers the module sources and
|
|
5998
|
-
|
|
5999
|
-
|
|
5996
|
+
offline regressions. `src/standard-library.js` registers the module sources and private control and
|
|
5997
|
+
constraint adapters for Node and browser resolution. Explicit `use_module/1-2`
|
|
5998
|
+
loads remain supported; outside strict ISO mode, the conservative interop
|
|
5999
|
+
autoloader may also load a module for one uniquely mapped common predicate.
|
|
6000
6000
|
The core registry remains available through `createDefaultRegistry()` and
|
|
6001
6001
|
`getDefaultRegistry()` for low-level embedding. The stricter Part 1 +
|
|
6002
6002
|
Corrigenda registry is exposed as `createStrictIsoRegistry()` and
|
|
@@ -6018,7 +6018,7 @@ between solution branches.
|
|
|
6018
6018
|
| `library(comparison)` | `lt/2`, `gt/2`, `le/2`, `ge/2` |
|
|
6019
6019
|
| `library(dates)` | `difference/3` |
|
|
6020
6020
|
| `library(iso_ext)` | `countall/2`, `forall/2`, `succ/2`, `cfor/3`, `findall/4`, `variant/2` |
|
|
6021
|
-
| `library(lists)` | `
|
|
6021
|
+
| `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`, `countall/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
|
|
6022
6022
|
| `library(primes)` | `smallest_divisor_from/3` |
|
|
6023
6023
|
| `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` |
|
|
6024
6024
|
| `library(random)` | `random/3` |
|
|
@@ -6027,6 +6027,35 @@ between solution branches.
|
|
|
6027
6027
|
|
|
6028
6028
|
<!-- eyeprolog-library-catalog:end -->
|
|
6029
6029
|
|
|
6030
|
+
#### Interoperability profile and conservative autoloading
|
|
6031
|
+
|
|
6032
|
+
The full EyeProlog library is larger than the deliberately conservative
|
|
6033
|
+
EyeProlog/Trealla/Scryer source-interoperability profile. The two concepts are
|
|
6034
|
+
kept separate: a predicate can be implemented as ordinary portable Prolog and
|
|
6035
|
+
still have an API that is not shared by the other systems.
|
|
6036
|
+
|
|
6037
|
+
`library(lists)` is the first aligned common module. Its interop surface includes
|
|
6038
|
+
`member/2`, `memberchk/2`, `select/3`, `append/2-3`, `last/2`,
|
|
6039
|
+
`same_length/2`, `nth0/3-4`, `nth1/3-4`, `reverse/2`, `length/2`,
|
|
6040
|
+
`maplist/2-8`, `foldl/4-6`, `sum_list/2`, and `list_to_set/2`. EyeProlog-only
|
|
6041
|
+
helpers such as `set_nth0/4`, `take/3`, `drop/3`, and `slice/4` remain available
|
|
6042
|
+
for compatibility but are outside this profile.
|
|
6043
|
+
|
|
6044
|
+
Normal EyeProlog execution can autoload an otherwise undefined unqualified call
|
|
6045
|
+
only when the interop table assigns it one canonical provider. Thus `member/2`
|
|
6046
|
+
autoloads from `library(lists)`. `between/3` is also in the interop profile;
|
|
6047
|
+
EyeProlog can satisfy it from its internal `library(prologue)` implementation
|
|
6048
|
+
without forcing portable source to mention that EyeProlog-specific library
|
|
6049
|
+
name. Autoloading is disabled by `--no-autoload`, by the JavaScript option
|
|
6050
|
+
`autoload: false`, and always by `--iso-strict`.
|
|
6051
|
+
|
|
6052
|
+
`-w` / `--warnings` reports explicit non-profile library dependencies and calls
|
|
6053
|
+
to non-profile predicates from common libraries. `--portable` turns those
|
|
6054
|
+
portability diagnostics into a failing command, making the profile suitable for
|
|
6055
|
+
continuous integration. `npm run test:interop` executes the same Sudoku source
|
|
6056
|
+
under EyeProlog, Trealla, and Scryer when those commands are installed; the
|
|
6057
|
+
repository's interoperability workflow installs them and runs that check.
|
|
6058
|
+
|
|
6030
6059
|
`library(clpz)` follows the Trealla and Scryer convention for constraint logic
|
|
6031
6060
|
programming over integers. Its first implementation step provides finite
|
|
6032
6061
|
interval and union domains, arithmetic and reified constraints, backtrackable
|
|
@@ -6420,7 +6449,9 @@ make the observed question explicit.
|
|
|
6420
6449
|
| `-h`, `--help` | Show usage |
|
|
6421
6450
|
| `-p`, `--proof` | Print `why/2` explanations |
|
|
6422
6451
|
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
6423
|
-
| `--iso-strict` | Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions and disable automatic tabling |
|
|
6452
|
+
| `--iso-strict` | Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions, disable interop autoloading, and disable automatic tabling |
|
|
6453
|
+
| `--portable` | Enforce the conservative EyeProlog/Trealla/Scryer interoperability profile |
|
|
6454
|
+
| `--no-autoload` | Disable conservative interop predicate autoloading |
|
|
6424
6455
|
| `-s`, `--stats` | Print final solver and memory statistics to stderr after execution |
|
|
6425
6456
|
| `-v`, `--version` | Print the package version |
|
|
6426
6457
|
| `-w`, `--warnings` | Print non-fatal portability warnings |
|
|
@@ -6434,8 +6465,9 @@ retains the Part 1 prefix `(?-)/1` operator and treats `-->/2` as ordinary Part
|
|
|
6434
6465
|
1 operator syntax; it does not perform Part 3 grammar-rule expansion or expose
|
|
6435
6466
|
`phrase/2-3`. Part 2 module directives and EyeProlog libraries are rejected,
|
|
6436
6467
|
the implementation-specific `occurs_check` flag is absent, and automatic
|
|
6437
|
-
tabling/recursion guards are disabled. Normal mode
|
|
6438
|
-
|
|
6468
|
+
tabling/recursion guards are disabled. Normal mode continues to support Parts
|
|
6469
|
+
2–3 and EyeProlog extensions, plus conservative interop autoloading for the
|
|
6470
|
+
profile documented above.
|
|
6439
6471
|
|
|
6440
6472
|
Inputs may be local files, HTTP(S) URLs, or one `-` for stdin. The bare command
|
|
6441
6473
|
`eyeprolog` starts the normal REPL; `eyeprolog --iso-strict` starts the strict
|
|
@@ -6462,7 +6494,7 @@ Work in a fixed sequence:
|
|
|
6462
6494
|
2. run without observation flags and compare stdout with that prediction;
|
|
6463
6495
|
3. add `--proof` when the support for an answer is the question;
|
|
6464
6496
|
4. add `--warnings` when portability or negative dependencies are the
|
|
6465
|
-
question;
|
|
6497
|
+
question; use `--portable` when non-profile dependencies must fail CI;
|
|
6466
6498
|
5. add `--stats` only when comparing two executions of the same semantic case.
|
|
6467
6499
|
|
|
6468
6500
|
For example:
|
|
@@ -6471,6 +6503,7 @@ For example:
|
|
|
6471
6503
|
eyeprolog --goal 'ancestor(X, Y)' examples/ancestor.pl
|
|
6472
6504
|
eyeprolog --proof --goal 'type(X, Y)' examples/socrates.pl
|
|
6473
6505
|
eyeprolog --warnings --goal 'answer(X)' test/conformance/warnings/negation/unstratified_mutual.pl
|
|
6506
|
+
eyeprolog --portable --goal 'sudoku_solution(S)' examples/sudoku.pl
|
|
6474
6507
|
eyeprolog --stats --goal 'path(a, X)' examples/path-discovery.pl > answers.pl 2> run.stats
|
|
6475
6508
|
```
|
|
6476
6509
|
|