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 CHANGED
@@ -123,8 +123,8 @@ noun --> [world] | [prolog].
123
123
  %% goal: phrase(sentence, Words)
124
124
  ```
125
125
 
126
- EyeProlog also adds 99 public library predicate indicators to its 129-entry ISO
127
- profile. **60 are implemented entirely as ordinary Prolog clauses** in focused
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,5 +1,4 @@
1
1
  % From The Art of EyeProlog, Chapter 6.
2
- :- use_module(library(prologue), [between/3]).
3
2
  :- use_module(library(lists)).
4
3
 
5
4
  square(N, Square) :-
@@ -1,3 +1,5 @@
1
+ :- use_module(library(lists), [memberchk/2]).
2
+
1
3
  % Fast Fourier Transform
2
4
  % Code from the book "Clause and Effect" Chapter 10
3
5
 
@@ -1 +1 @@
1
- fft([0, 1, 2, 3, 4, 5, 6, 7], [n(232, op(+, 216, 231)), n(231, op(*, 217, 230)), n(230, op(+, 222, 229)), n(229, op(*, 223, 228)), n(228, op(+, 224, 227)), n(227, op(*, 225, 226)), n(226, a(7)), n(225, w ^ 4), n(224, a(3)), n(223, w ^ 6), n(222, op(+, 218, 221)), n(221, op(*, 219, 220)), n(220, a(5)), n(219, w ^ 4), n(218, a(1)), n(217, w ^ 7), n(216, op(+, 208, 215)), n(215, op(*, 209, 214)), n(214, op(+, 210, 213)), n(213, op(*, 211, 212)), n(212, a(6)), n(211, w ^ 4), n(210, a(2)), n(209, w ^ 6), n(208, op(+, 204, 207)), n(207, op(*, 205, 206)), n(206, a(4)), n(205, w ^ 4), n(204, a(0)), n(203, op(+, 187, 202)), n(202, op(*, 188, 201)), n(201, op(+, 193, 200)), n(200, op(*, 194, 199)), n(199, op(+, 195, 198)), n(198, op(*, 196, 197)), n(197, a(7)), n(196, w ^ 0), n(195, a(3)), n(194, w ^ 4), n(193, op(+, 189, 192)), n(192, op(*, 190, 191)), n(191, a(5)), n(190, w ^ 0), n(189, a(1)), n(188, w ^ 6), n(187, op(+, 179, 186)), n(186, op(*, 180, 185)), n(185, op(+, 181, 184)), n(184, op(*, 182, 183)), n(183, a(6)), n(182, w ^ 0), n(181, a(2)), n(180, w ^ 4), n(179, op(+, 175, 178)), n(178, op(*, 176, 177)), n(177, a(4)), n(176, w ^ 0), n(175, a(0)), n(174, op(+, 158, 173)), n(173, op(*, 159, 172)), n(172, op(+, 164, 171)), n(171, op(*, 165, 170)), n(170, op(+, 166, 169)), n(169, op(*, 167, 168)), n(168, a(7)), n(167, w ^ 4), n(166, a(3)), n(165, w ^ 2), n(164, op(+, 160, 163)), n(163, op(*, 161, 162)), n(162, a(5)), n(161, w ^ 4), n(160, a(1)), n(159, w ^ 5), n(158, op(+, 150, 157)), n(157, op(*, 151, 156)), n(156, op(+, 152, 155)), n(155, op(*, 153, 154)), n(154, a(6)), n(153, w ^ 4), n(152, a(2)), n(151, w ^ 2), n(150, op(+, 146, 149)), n(149, op(*, 147, 148)), n(148, a(4)), n(147, w ^ 4), n(146, a(0)), n(145, op(+, 129, 144)), n(144, op(*, 130, 143)), n(143, op(+, 135, 142)), n(142, op(*, 136, 141)), n(141, op(+, 137, 140)), n(140, op(*, 138, 139)), n(139, a(7)), n(138, w ^ 0), n(137, a(3)), n(136, w ^ 0), n(135, op(+, 131, 134)), n(134, op(*, 132, 133)), n(133, a(5)), n(132, w ^ 0), n(131, a(1)), n(130, w ^ 4), n(129, op(+, 121, 128)), n(128, op(*, 122, 127)), n(127, op(+, 123, 126)), n(126, op(*, 124, 125)), n(125, a(6)), n(124, w ^ 0), n(123, a(2)), n(122, w ^ 0), n(121, op(+, 117, 120)), n(120, op(*, 118, 119)), n(119, a(4)), n(118, w ^ 0), n(117, a(0)), n(116, op(+, 100, 115)), n(115, op(*, 101, 114)), n(114, op(+, 106, 113)), n(113, op(*, 107, 112)), n(112, op(+, 108, 111)), n(111, op(*, 109, 110)), n(110, a(7)), n(109, w ^ 4), n(108, a(3)), n(107, w ^ 6), n(106, op(+, 102, 105)), n(105, op(*, 103, 104)), n(104, a(5)), n(103, w ^ 4), n(102, a(1)), n(101, w ^ 3), n(100, op(+, 92, 99)), n(99, op(*, 93, 98)), n(98, op(+, 94, 97)), n(97, op(*, 95, 96)), n(96, a(6)), n(95, w ^ 4), n(94, a(2)), n(93, w ^ 6), n(92, op(+, 88, 91)), n(91, op(*, 89, 90)), n(90, a(4)), n(89, w ^ 4), n(88, a(0)), n(87, op(+, 71, 86)), n(86, op(*, 72, 85)), n(85, op(+, 77, 84)), n(84, op(*, 78, 83)), n(83, op(+, 79, 82)), n(82, op(*, 80, 81)), n(81, a(7)), n(80, w ^ 0), n(79, a(3)), n(78, w ^ 4), n(77, op(+, 73, 76)), n(76, op(*, 74, 75)), n(75, a(5)), n(74, w ^ 0), n(73, a(1)), n(72, w ^ 2), n(71, op(+, 63, 70)), n(70, op(*, 64, 69)), n(69, op(+, 65, 68)), n(68, op(*, 66, 67)), n(67, a(6)), n(66, w ^ 0), n(65, a(2)), n(64, w ^ 4), n(63, op(+, 59, 62)), n(62, op(*, 60, 61)), n(61, a(4)), n(60, w ^ 0), n(59, a(0)), n(58, op(+, 42, 57)), n(57, op(*, 43, 56)), n(56, op(+, 48, 55)), n(55, op(*, 49, 54)), n(54, op(+, 50, 53)), n(53, op(*, 51, 52)), n(52, a(7)), n(51, w ^ 4), n(50, a(3)), n(49, w ^ 2), n(48, op(+, 44, 47)), n(47, op(*, 45, 46)), n(46, a(5)), n(45, w ^ 4), n(44, a(1)), n(43, w ^ 1), n(42, op(+, 34, 41)), n(41, op(*, 35, 40)), n(40, op(+, 36, 39)), n(39, op(*, 37, 38)), n(38, a(6)), n(37, w ^ 4), n(36, a(2)), n(35, w ^ 2), n(34, op(+, 30, 33)), n(33, op(*, 31, 32)), n(32, a(4)), n(31, w ^ 4), n(30, a(0)), n(29, op(+, 13, 28)), n(28, op(*, 14, 27)), n(27, op(+, 19, 26)), n(26, op(*, 20, 25)), n(25, op(+, 21, 24)), n(24, op(*, 22, 23)), n(23, a(7)), n(22, w ^ 0), n(21, a(3)), n(20, w ^ 0), n(19, op(+, 15, 18)), n(18, op(*, 16, 17)), n(17, a(5)), n(16, w ^ 0), n(15, a(1)), n(14, w ^ 0), n(13, op(+, 5, 12)), n(12, op(*, 6, 11)), n(11, op(+, 7, 10)), n(10, op(*, 8, 9)), n(9, a(6)), n(8, w ^ 0), n(7, a(2)), n(6, w ^ 0), n(5, op(+, 1, 4)), n(4, op(*, 2, 3)), n(3, a(4)), n(2, w ^ 0), n(1, a(0))]).
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))]).
@@ -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
- set_nth0(Column, Cells, Digit, UpdatedCells),
113
- set_nth0(Row, Grid, UpdatedCells, Result).
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.2.44",
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
- if (options.warnings) printWarnings(program);
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 parseOptions = { ...options, sourceMetadata: includeWhy || requestedStrictIso };
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
- % Load this module explicitly with use_module(library(lists)). The module
30
- % boundary keeps these widespread names separate from same-named predicates in
31
- % other modules.
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
- nth0(0, [X|_], X).
51
- nth0(N, [_|Xs], X) :- var(N), nth0(N0, Xs, X), N is N0 + 1.
52
- nth0(N, [_|Xs], X) :- nonvar(N), N > 0, N1 is N - 1, nth0(N1, Xs, X).
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, X) :- nth0(N0, List, X), N is N0 + 1.
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 { standardLibrarySources } from './standard-library.js';
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 };
@@ -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
- // unlike the former autoloader, no clauses are added unless use_module/1 or
4
- // use_module/2 requests their module.
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`);
@@ -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, 59, 'portable Prolog library count');
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, 99, 'complete EyeProlog library surface');
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');
@@ -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 **99 library predicate indicators** in addition to the 129
5979
- indicators in its isolated ISO profile. **60 are defined as ordinary Prolog
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
- private control and constraint adapters for Node and browser resolution, and never adds clauses
5999
- implicitly.
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)` | `maplist/3`, `append/3`, `member/2`, `select/3`, `last/2`, `nth0/3`, `nth1/3`, `reverse/2`, `length/2`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `countall/2`, `set_nth0/4`, `take/3`, `drop/3`, `slice/4` |
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 is unchanged and continues
6438
- to support Parts 2–3 and EyeProlog extensions.
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