eyeprolog 1.3.0 → 1.3.2
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 +8 -5
- package/package.json +1 -1
- package/src/lib/lists.pl +30 -6
- package/src/repl.js +61 -11
- package/src/standard-library.js +5 -1
- package/test/run-regression.mjs +165 -1
- package/the-art-of-eyeprolog.md +24 -12
package/README.md
CHANGED
|
@@ -138,14 +138,17 @@ predicates use ISO atoms or character lists. The old catch-all
|
|
|
138
138
|
EyeProlog keeps a conservative source-level interoperability profile separate
|
|
139
139
|
from the larger EyeProlog library surface. `library(lists)` follows the common
|
|
140
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
|
|
141
|
+
`append/2-3`, `nth0/3-4`, `nth1/3-4`, `length/2`, `maplist/2-8`, and
|
|
142
|
+
`foldl/4-6`. Its `length/2` remains relational: with both arguments variable,
|
|
143
|
+
`length(Xs, N)` enumerates lists of increasing length together with `N = 0, 1,
|
|
144
|
+
2, ...`.
|
|
142
145
|
|
|
143
146
|
Outside `--iso-strict`, an otherwise undefined unqualified call may autoload a
|
|
144
147
|
predicate only when the interop profile has one canonical EyeProlog provider.
|
|
145
|
-
For example, `member/2` autoloads from `library(lists)
|
|
146
|
-
EyeProlog's internal Prologue implementation without
|
|
147
|
-
to name `library(prologue)`. Use `--no-autoload` to
|
|
148
|
-
Strict ISO mode never autoloads library predicates.
|
|
148
|
+
For example, `member/2` autoloads from `library(lists)`, while `between/3` and
|
|
149
|
+
`call_nth/2` can use EyeProlog's internal Prologue implementation without
|
|
150
|
+
requiring portable source to name `library(prologue)`. Use `--no-autoload` to
|
|
151
|
+
disable this convenience. Strict ISO mode never autoloads library predicates.
|
|
149
152
|
|
|
150
153
|
Use `-w` / `--warnings` to diagnose dependencies outside the interop profile,
|
|
151
154
|
or `--portable` to make such diagnostics fail the run. This catches both
|
package/package.json
CHANGED
package/src/lib/lists.pl
CHANGED
|
@@ -133,8 +133,17 @@ nth1(N, List, Elem, Rest) :-
|
|
|
133
133
|
|
|
134
134
|
reverse(List, Reversed) :- lists__reverse(List, [], Reversed).
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
length(List, Length)
|
|
136
|
+
% Keep the common lists:length/2 relation fully relational. In particular,
|
|
137
|
+
% length(List, Length) with both arguments variable enumerates lists of
|
|
138
|
+
% increasing length, as required by portable generators such as the issue #28
|
|
139
|
+
% number_chars/2 stress test.
|
|
140
|
+
length(List, Length) :-
|
|
141
|
+
nonvar(Length), !,
|
|
142
|
+
lists__integer(Length),
|
|
143
|
+
lists__not_less_than_zero(Length),
|
|
144
|
+
lists__length_fixed(Length, List).
|
|
145
|
+
length(List, Length) :-
|
|
146
|
+
lists__length_generate(List, 0, Length).
|
|
138
147
|
|
|
139
148
|
foldl(_, [], Acc, Acc).
|
|
140
149
|
foldl(Closure, [A|As], Acc0, Acc) :-
|
|
@@ -186,10 +195,25 @@ slice(Start, Count, List, Slice) :-
|
|
|
186
195
|
lists__reverse([], Acc, Acc).
|
|
187
196
|
lists__reverse([X|Xs], Acc, Out) :- lists__reverse(Xs, [X|Acc], Out).
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
lists__length_fixed(0, []).
|
|
199
|
+
lists__length_fixed(N, [_|Xs]) :-
|
|
200
|
+
N > 0,
|
|
201
|
+
N1 is N - 1,
|
|
202
|
+
lists__length_fixed(N1, Xs).
|
|
203
|
+
|
|
204
|
+
lists__length_generate([], N, N).
|
|
205
|
+
lists__length_generate([_|Xs], N0, N) :-
|
|
206
|
+
N1 is N0 + 1,
|
|
207
|
+
lists__length_generate(Xs, N1, N).
|
|
208
|
+
|
|
209
|
+
lists__integer(X) :- integer(X), !.
|
|
210
|
+
lists__integer(X) :- var(X), !, 0 is X.
|
|
211
|
+
% arg/3 performs the ISO integer type check before inspecting its term.
|
|
212
|
+
lists__integer(X) :- arg(X, type_check, _).
|
|
213
|
+
|
|
214
|
+
lists__not_less_than_zero(X) :- X >= 0, !.
|
|
215
|
+
% atom_length/2 reports domain_error(not_less_than_zero) for a negative value.
|
|
216
|
+
lists__not_less_than_zero(X) :- atom_length('', X).
|
|
193
217
|
|
|
194
218
|
lists__sum_list([], Sum, Sum).
|
|
195
219
|
lists__sum_list([X|Xs], Acc, Sum) :- Next is Acc + X, lists__sum_list(Xs, Next, Sum).
|
package/src/repl.js
CHANGED
|
@@ -55,7 +55,10 @@ export async function runRepl(engine, options = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
const consultFiles = options.isoStrict ? null : consultDesignations(engine, goal);
|
|
57
57
|
if (consultFiles != null) {
|
|
58
|
-
for (const filename of consultFiles)
|
|
58
|
+
for (const filename of consultFiles) {
|
|
59
|
+
const source = await readSource(filename);
|
|
60
|
+
replaceConsultedSource(sources, source);
|
|
61
|
+
}
|
|
59
62
|
state = makeState(engine, sources, output, options, state, reader);
|
|
60
63
|
runWithTerminalSignals(reader, () => state.solver.runInitializations());
|
|
61
64
|
output.write(' true.\n');
|
|
@@ -447,10 +450,32 @@ function isHaltGoal(goal) {
|
|
|
447
450
|
}
|
|
448
451
|
|
|
449
452
|
function consultDesignations(engine, goal) {
|
|
453
|
+
// The traditional [file]. top-level shorthand and explicit consult/1 use
|
|
454
|
+
// the same resolver and modern reconsult semantics. Accept reconsult/1 as
|
|
455
|
+
// a compatibility alias as well; contemporary consult/1 already replaces
|
|
456
|
+
// clauses previously loaded from the same source.
|
|
450
457
|
if (goal.type === 'atom' && goal.name === '[]') return [];
|
|
451
|
-
if (goal.type
|
|
452
|
-
|
|
453
|
-
|
|
458
|
+
if (goal.type === 'compound' && goal.name === '.' && goal.arity === 2) {
|
|
459
|
+
return consultListDesignations(engine, goal);
|
|
460
|
+
}
|
|
461
|
+
if (goal.type === 'compound' && ['consult', 'reconsult'].includes(goal.name) && goal.arity === 1) {
|
|
462
|
+
return consultArgumentDesignations(engine, goal.args[0]);
|
|
463
|
+
}
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function consultArgumentDesignations(engine, term) {
|
|
468
|
+
if (term.type === 'var') throw new engine.PrologError('instantiation_error');
|
|
469
|
+
if (term.type === 'atom') return term.name === '[]' ? [] : [term.name];
|
|
470
|
+
if (term.type === 'compound' && term.name === '.' && term.arity === 2) {
|
|
471
|
+
return consultListDesignations(engine, term);
|
|
472
|
+
}
|
|
473
|
+
throw new engine.PrologError('type_error(atom)', term);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function consultListDesignations(engine, list) {
|
|
477
|
+
const items = engine.properListItems(list, new engine.Env());
|
|
478
|
+
if (items == null) throw new engine.PrologError('type_error(list)', list);
|
|
454
479
|
return items.map((item) => {
|
|
455
480
|
if (item.type === 'var') throw new engine.PrologError('instantiation_error');
|
|
456
481
|
if (item.type !== 'atom') throw new engine.PrologError('type_error(atom)', item);
|
|
@@ -458,18 +483,43 @@ function consultDesignations(engine, goal) {
|
|
|
458
483
|
});
|
|
459
484
|
}
|
|
460
485
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
function replaceConsultedSource(sources, source) {
|
|
487
|
+
const index = sources.findIndex((existing) =>
|
|
488
|
+
source.consultPath != null && existing.consultPath === source.consultPath);
|
|
489
|
+
if (index >= 0) sources[index] = source;
|
|
490
|
+
else sources.push(source);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function missingSource(error) {
|
|
494
|
+
return error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function resolvedConsultFilename(designation) {
|
|
498
|
+
const requested = path.resolve(designation);
|
|
499
|
+
// Long-standing Prolog consult convention: for an extensionless name, try
|
|
500
|
+
// the .pl source first and use the unsuffixed path only as a fallback.
|
|
501
|
+
if (!path.extname(requested)) {
|
|
502
|
+
const prologFile = `${requested}.pl`;
|
|
503
|
+
try {
|
|
504
|
+
await fs.access(prologFile);
|
|
505
|
+
return await fs.realpath(prologFile);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (!missingSource(error)) throw error;
|
|
508
|
+
}
|
|
468
509
|
}
|
|
510
|
+
await fs.access(requested);
|
|
511
|
+
return fs.realpath(requested);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function readSource(designation) {
|
|
515
|
+
const filename = await resolvedConsultFilename(designation);
|
|
469
516
|
return {
|
|
470
517
|
text: await fs.readFile(filename, 'utf8'),
|
|
471
518
|
filename: path.basename(filename),
|
|
472
519
|
baseDir: path.dirname(filename),
|
|
520
|
+
// Keep host-only provenance so consulting the same resolved file again
|
|
521
|
+
// replaces its previous source instead of accumulating stale clauses.
|
|
522
|
+
consultPath: filename,
|
|
473
523
|
};
|
|
474
524
|
}
|
|
475
525
|
|
package/src/standard-library.js
CHANGED
|
@@ -109,6 +109,10 @@ export const eyePrologInteropAutoload = Object.freeze({
|
|
|
109
109
|
'foldl/6': 'lists',
|
|
110
110
|
'sum_list/2': 'lists',
|
|
111
111
|
'list_to_set/2': 'lists',
|
|
112
|
+
// call_nth/2 is available in both Trealla and Scryer (the latter via
|
|
113
|
+
// library(iso_ext)). EyeProlog keeps its adapter in library(prologue), so
|
|
114
|
+
// autoloading hides that implementation-specific location from source.
|
|
115
|
+
'call_nth/2': 'prologue',
|
|
112
116
|
// Trealla and Scryer expose between/3 without an EyeProlog-style
|
|
113
117
|
// library(prologue) dependency. EyeProlog keeps its implementation in the
|
|
114
118
|
// Prologue module but autoloads it so portable source need not name that
|
|
@@ -150,7 +154,7 @@ function* statisticsValueBuiltin({ solver, goal, env }) {
|
|
|
150
154
|
|
|
151
155
|
if (entries == null) {
|
|
152
156
|
if (key.type !== ATOM) throw new PrologError('type_error(atom)', key);
|
|
153
|
-
|
|
157
|
+
throw new PrologError('domain_error(statistics_key)', key);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
for (const [name, value] of entries) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -1712,6 +1712,143 @@ c4 ?- call((!;1)).
|
|
|
1712
1712
|
assertEqual(result.stderr, '', 'stderr');
|
|
1713
1713
|
},
|
|
1714
1714
|
},
|
|
1715
|
+
{
|
|
1716
|
+
name: 'REPL consult prefers .pl over an unsuffixed file (issue #47)',
|
|
1717
|
+
run: () => {
|
|
1718
|
+
const stem = path.join(tmp, `repl-consult-order-${++tmpCounter}`);
|
|
1719
|
+
fs.writeFileSync(stem, 'chosen(bare).\n');
|
|
1720
|
+
fs.writeFileSync(`${stem}.pl`, 'chosen(pl).\n');
|
|
1721
|
+
const result = runCli([], {
|
|
1722
|
+
input: `[${sourceAtom(stem)}].\nchosen(X).\nhalt.\n`,
|
|
1723
|
+
});
|
|
1724
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1725
|
+
assertIncludes(result.stdout, 'X = pl.', 'consulted .pl source');
|
|
1726
|
+
assertNotIncludes(result.stdout, 'X = bare', 'unsuffixed source is fallback only');
|
|
1727
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1728
|
+
|
|
1729
|
+
const explicit = runCli([], {
|
|
1730
|
+
input: `consult(${sourceAtom(stem)}).\nchosen(X).\nhalt.\n`,
|
|
1731
|
+
});
|
|
1732
|
+
assertEqual(explicit.status, 0, 'consult/1 exit status');
|
|
1733
|
+
assertIncludes(explicit.stdout, 'X = pl.', 'consult/1 prefers .pl source');
|
|
1734
|
+
assertNotIncludes(explicit.stdout, 'X = bare', 'consult/1 does not prefer unsuffixed source');
|
|
1735
|
+
assertEqual(explicit.stderr, '', 'consult/1 stderr');
|
|
1736
|
+
|
|
1737
|
+
fs.rmSync(`${stem}.pl`);
|
|
1738
|
+
const fallback = runCli([], {
|
|
1739
|
+
input: `[${sourceAtom(stem)}].\nchosen(X).\nhalt.\n`,
|
|
1740
|
+
});
|
|
1741
|
+
assertEqual(fallback.status, 0, 'fallback exit status');
|
|
1742
|
+
assertIncludes(fallback.stdout, 'X = bare.', 'unsuffixed fallback source');
|
|
1743
|
+
assertEqual(fallback.stderr, '', 'fallback stderr');
|
|
1744
|
+
},
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
name: 'REPL consultation replaces earlier clauses from the same file (issue #46)',
|
|
1748
|
+
run: () => {
|
|
1749
|
+
const filename = path.join(tmp, `repl-reconsult-${++tmpCounter}.pl`);
|
|
1750
|
+
fs.writeFileSync(filename, 'factum(f).\n');
|
|
1751
|
+
const harness = path.join(tmp, `repl-reconsult-harness-${++tmpCounter}.mjs`);
|
|
1752
|
+
const consultedAtom = sourceAtom(filename);
|
|
1753
|
+
fs.writeFileSync(harness, `
|
|
1754
|
+
import fs from 'node:fs';
|
|
1755
|
+
import process from 'node:process';
|
|
1756
|
+
import { spawn } from 'node:child_process';
|
|
1757
|
+
|
|
1758
|
+
const child = spawn(process.execPath, [${JSON.stringify(bin)}], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1759
|
+
let stdout = '';
|
|
1760
|
+
let stderr = '';
|
|
1761
|
+
let advanced = false;
|
|
1762
|
+
let failed = false;
|
|
1763
|
+
const timer = setTimeout(() => {
|
|
1764
|
+
failed = true;
|
|
1765
|
+
child.kill();
|
|
1766
|
+
}, 5000);
|
|
1767
|
+
|
|
1768
|
+
child.stdout.setEncoding('utf8');
|
|
1769
|
+
child.stderr.setEncoding('utf8');
|
|
1770
|
+
child.stdout.on('data', (chunk) => {
|
|
1771
|
+
stdout += chunk;
|
|
1772
|
+
if (!advanced && stdout.includes('?- true.\\n?- ')) {
|
|
1773
|
+
advanced = true;
|
|
1774
|
+
fs.writeFileSync(${JSON.stringify(filename)}, 'factum(g).\\n');
|
|
1775
|
+
child.stdin.write(\`[${consultedAtom}].\\nfindall(F,factum(F),Fs).\\nhalt.\\n\`);
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
1779
|
+
child.on('close', (code) => {
|
|
1780
|
+
clearTimeout(timer);
|
|
1781
|
+
process.stdout.write(stdout);
|
|
1782
|
+
process.stderr.write(stderr);
|
|
1783
|
+
process.exitCode = failed ? 99 : (code ?? 98);
|
|
1784
|
+
});
|
|
1785
|
+
child.stdin.write(\`[${consultedAtom}].\\n\`);
|
|
1786
|
+
`);
|
|
1787
|
+
const result = spawnSync(process.execPath, [harness], {
|
|
1788
|
+
cwd: packageRoot,
|
|
1789
|
+
encoding: 'utf8',
|
|
1790
|
+
timeout: 7000,
|
|
1791
|
+
});
|
|
1792
|
+
assertEqual(result.error?.code, undefined, 'reconsult harness timeout');
|
|
1793
|
+
assertEqual(result.status, 0, `exit status; stderr=${result.stderr}`);
|
|
1794
|
+
assertIncludes(result.stdout, 'Fs = \"g\".', 'reconsulted clauses');
|
|
1795
|
+
assertNotIncludes(result.stdout, 'f', 'stale clause removed');
|
|
1796
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1797
|
+
},
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
name: 'REPL consult/1 has reconsult semantics',
|
|
1801
|
+
run: () => {
|
|
1802
|
+
const filename = path.join(tmp, `repl-consult-predicate-${++tmpCounter}.pl`);
|
|
1803
|
+
fs.writeFileSync(filename, 'factum(f).\n');
|
|
1804
|
+
const harness = path.join(tmp, `repl-consult-predicate-harness-${++tmpCounter}.mjs`);
|
|
1805
|
+
const consultedAtom = sourceAtom(filename);
|
|
1806
|
+
fs.writeFileSync(harness, `
|
|
1807
|
+
import fs from 'node:fs';
|
|
1808
|
+
import process from 'node:process';
|
|
1809
|
+
import { spawn } from 'node:child_process';
|
|
1810
|
+
|
|
1811
|
+
const child = spawn(process.execPath, [${JSON.stringify(bin)}], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
1812
|
+
let stdout = '';
|
|
1813
|
+
let stderr = '';
|
|
1814
|
+
let advanced = false;
|
|
1815
|
+
let failed = false;
|
|
1816
|
+
const timer = setTimeout(() => {
|
|
1817
|
+
failed = true;
|
|
1818
|
+
child.kill();
|
|
1819
|
+
}, 5000);
|
|
1820
|
+
|
|
1821
|
+
child.stdout.setEncoding('utf8');
|
|
1822
|
+
child.stderr.setEncoding('utf8');
|
|
1823
|
+
child.stdout.on('data', (chunk) => {
|
|
1824
|
+
stdout += chunk;
|
|
1825
|
+
if (!advanced && stdout.includes('?- true.\\n?- ')) {
|
|
1826
|
+
advanced = true;
|
|
1827
|
+
fs.writeFileSync(${JSON.stringify(filename)}, 'factum(g).\\n');
|
|
1828
|
+
child.stdin.write(\`consult(${consultedAtom}).\\nfindall(F,factum(F),Fs).\\nhalt.\\n\`);
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
1832
|
+
child.on('close', (code) => {
|
|
1833
|
+
clearTimeout(timer);
|
|
1834
|
+
process.stdout.write(stdout);
|
|
1835
|
+
process.stderr.write(stderr);
|
|
1836
|
+
process.exitCode = failed ? 99 : (code ?? 98);
|
|
1837
|
+
});
|
|
1838
|
+
child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
1839
|
+
`);
|
|
1840
|
+
const result = spawnSync(process.execPath, [harness], {
|
|
1841
|
+
cwd: packageRoot,
|
|
1842
|
+
encoding: 'utf8',
|
|
1843
|
+
timeout: 7000,
|
|
1844
|
+
});
|
|
1845
|
+
assertEqual(result.error?.code, undefined, 'consult/1 reconsult harness timeout');
|
|
1846
|
+
assertEqual(result.status, 0, `exit status; stderr=${result.stderr}`);
|
|
1847
|
+
assertIncludes(result.stdout, 'Fs = "g".', 'consult/1 replaced earlier clauses');
|
|
1848
|
+
assertNotIncludes(result.stdout, 'f', 'consult/1 removed stale clause');
|
|
1849
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1715
1852
|
{
|
|
1716
1853
|
name: 'REPL preserves runtime unknown flag across consultation',
|
|
1717
1854
|
run: () => {
|
|
@@ -2002,6 +2139,17 @@ c4 ?- call((!;1)).
|
|
|
2002
2139
|
assertEqual(result.stderr, '', 'stderr');
|
|
2003
2140
|
},
|
|
2004
2141
|
},
|
|
2142
|
+
{
|
|
2143
|
+
name: 'statistics/2 rejects unknown keys instead of silently failing (issue #45)',
|
|
2144
|
+
run: () => {
|
|
2145
|
+
const result = runCli([], { input: 'statistics(nonsense, Value).\nhalt.\n' });
|
|
2146
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2147
|
+
assertIncludes(result.stdout,
|
|
2148
|
+
'error(domain_error(statistics_key, nonsense), eyeprolog).',
|
|
2149
|
+
'statistics key error');
|
|
2150
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2151
|
+
},
|
|
2152
|
+
},
|
|
2005
2153
|
{
|
|
2006
2154
|
name: 'statistics predicates are excluded from strict ISO mode',
|
|
2007
2155
|
run: () => {
|
|
@@ -2030,6 +2178,21 @@ c4 ?- call((!;1)).
|
|
|
2030
2178
|
assertEqual(result.stderr, '', 'stderr');
|
|
2031
2179
|
},
|
|
2032
2180
|
},
|
|
2181
|
+
{
|
|
2182
|
+
name: 'library(lists) length/2 stays relational and call_nth/2 autoloads (issue #28)',
|
|
2183
|
+
run: () => {
|
|
2184
|
+
const input = [
|
|
2185
|
+
':- use_module(library(lists)).',
|
|
2186
|
+
'%% goal: fourth_length(N)',
|
|
2187
|
+
'fourth_length(N) :- call_nth(length(_Xs, N), 4).',
|
|
2188
|
+
'',
|
|
2189
|
+
].join('\n');
|
|
2190
|
+
const result = runCli(['-'], { input });
|
|
2191
|
+
assertEqual(result.status, 0, 'exit status');
|
|
2192
|
+
assertEqual(result.stdout, 'fourth_length(3).\n', 'stdout');
|
|
2193
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2033
2196
|
{
|
|
2034
2197
|
name: 'strict ISO mode disables interop autoloading',
|
|
2035
2198
|
run: () => {
|
|
@@ -3433,10 +3596,11 @@ check(A, B, C, D, E, F) :-
|
|
|
3433
3596
|
assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
|
|
3434
3597
|
assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
|
|
3435
3598
|
assertEqual(eyePrologPortableLibraryIndicators.length, 62, 'portable Prolog library count');
|
|
3436
|
-
assertEqual(eyePrologInteropLibraryIndicators.length,
|
|
3599
|
+
assertEqual(eyePrologInteropLibraryIndicators.length, 27, 'cross-implementation interop profile count');
|
|
3437
3600
|
assertEqual(eyePrologInteropLibraryModules.join(','), 'lists', 'common explicit library module profile');
|
|
3438
3601
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
3439
3602
|
assertEqual(eyePrologInteropAutoload['between/3'], 'prologue', 'between/3 canonical internal autoload');
|
|
3603
|
+
assertEqual(eyePrologInteropAutoload['call_nth/2'], 'prologue', 'call_nth/2 canonical internal autoload');
|
|
3440
3604
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
3441
3605
|
assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
|
|
3442
3606
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 2).join(','), 'call_nth/2,freeze/2', 'control predicates requiring host support');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -4646,8 +4646,10 @@ where a live snapshot is useful. For example, a long-running loop can include
|
|
|
4646
4646
|
immediately to the current output stream. `statistics/2` makes an individual
|
|
4647
4647
|
value available to the program, for example
|
|
4648
4648
|
`statistics(memory_guard_used_bytes, Used)`. With an unbound first argument it
|
|
4649
|
-
enumerates the available statistic keys and values.
|
|
4650
|
-
|
|
4649
|
+
enumerates the available statistic keys and values. An atom that is not an
|
|
4650
|
+
available key raises `domain_error(statistics_key, Key)` rather than silently
|
|
4651
|
+
failing. These predicates are EyeProlog observability extensions and are not
|
|
4652
|
+
available under `--iso-strict`.
|
|
4651
4653
|
|
|
4652
4654
|
Compare statistics only between runs with the same query, data, and observable
|
|
4653
4655
|
answer contract. A faster program that silently loses answers is not an
|
|
@@ -6037,17 +6039,21 @@ still have an API that is not shared by the other systems.
|
|
|
6037
6039
|
`library(lists)` is the first aligned common module. Its interop surface includes
|
|
6038
6040
|
`member/2`, `memberchk/2`, `select/3`, `append/2-3`, `last/2`,
|
|
6039
6041
|
`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`.
|
|
6041
|
-
|
|
6042
|
-
|
|
6042
|
+
`maplist/2-8`, `foldl/4-6`, `sum_list/2`, and `list_to_set/2`. Its `length/2`
|
|
6043
|
+
is fully relational: when both arguments are variables, `length(Xs, N)`
|
|
6044
|
+
enumerates `Xs = [], N = 0`, then one-element lists with `N = 1`, and so on.
|
|
6045
|
+
This generator mode is important for portable stress and enumeration programs.
|
|
6046
|
+
EyeProlog-only helpers such as `set_nth0/4`, `take/3`, `drop/3`, and `slice/4`
|
|
6047
|
+
remain available for compatibility but are outside this profile.
|
|
6043
6048
|
|
|
6044
6049
|
Normal EyeProlog execution can autoload an otherwise undefined unqualified call
|
|
6045
6050
|
only when the interop table assigns it one canonical provider. Thus `member/2`
|
|
6046
|
-
autoloads from `library(lists)`. `between/3`
|
|
6047
|
-
EyeProlog can satisfy
|
|
6048
|
-
without forcing portable source to mention
|
|
6049
|
-
name. Autoloading is disabled by
|
|
6050
|
-
`autoload: false`, and always by
|
|
6051
|
+
autoloads from `library(lists)`. `between/3` and `call_nth/2` are also in the
|
|
6052
|
+
interop profile; EyeProlog can satisfy them from its internal
|
|
6053
|
+
`library(prologue)` implementation without forcing portable source to mention
|
|
6054
|
+
that EyeProlog-specific library name. Autoloading is disabled by
|
|
6055
|
+
`--no-autoload`, by the JavaScript option `autoload: false`, and always by
|
|
6056
|
+
`--iso-strict`.
|
|
6051
6057
|
|
|
6052
6058
|
`-w` / `--warnings` reports explicit non-profile library dependencies and calls
|
|
6053
6059
|
to non-profile predicates from common libraries. `--portable` turns those
|
|
@@ -6395,8 +6401,14 @@ not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
|
|
|
6395
6401
|
for example `T = (a = b).` rather than the invalid `T = a = b.`. When an answer
|
|
6396
6402
|
ends in a graphic token, the top level inserts layout before its terminating
|
|
6397
6403
|
full stop so the two tokens cannot merge; for example `?- X = .* .` displays
|
|
6398
|
-
`X = .* .`, not `X = .*.`. Use `[file]
|
|
6399
|
-
consult local source
|
|
6404
|
+
`X = .* .`, not `X = .*.`. Use `[file].`, `['file.pl'].`, or
|
|
6405
|
+
`consult(file).` to consult local source; `reconsult(file).` is accepted as a
|
|
6406
|
+
compatibility alias. Use `halt.` or `halt(Status).` to leave the top level.
|
|
6407
|
+
For an extensionless designation such as `[file].` or `consult(file).`, the
|
|
6408
|
+
top level tries `file.pl` before the unsuffixed `file`. Both the shorthand and
|
|
6409
|
+
`consult/1` have modern reconsult semantics: consulting the same resolved file
|
|
6410
|
+
again replaces its previous source, so clauses removed from the file do not
|
|
6411
|
+
remain active.
|
|
6400
6412
|
When `read/1-2` or `read_term/2-3` actually reaches interactive
|
|
6401
6413
|
`user_input`, the top level requests the next full-stop-terminated Prolog term
|
|
6402
6414
|
with a `|: ` input prompt instead of treating the terminal stream as already
|