eyeprolog 1.5.76 → 1.5.78
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 +1 -1
- package/package.json +1 -1
- package/src/iso.js +6 -0
- package/test/bench/benchmarks.json +27 -10
- package/test/benchmark.mjs +38 -37
- package/test/regression/cases-regression.mjs +15 -0
- package/test/run-benchmark-tests.mjs +1 -15
- package/the-art-of-eyeprolog.md +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ The checked [Symbiotic Knowledge Graphs example](examples/symbiotic-knowledge-gr
|
|
|
84
84
|
The same RDF → Prolog → RDF boundary is exercised by five additional checked scenarios: [cross-organization data sharing](https://eyereasoner.github.io/eyeprolog/examples/deck/cross-organization-data-sharing), [explainable EV-depot configuration](https://eyereasoner.github.io/eyeprolog/examples/deck/explainable-ev-depot-configuration), [operational incident response](https://eyereasoner.github.io/eyeprolog/examples/deck/operational-incident-response), [software supply-chain vulnerability response](https://eyereasoner.github.io/eyeprolog/examples/deck/sbom-vulnerability-response), and a [scientific evidence graph](https://eyereasoner.github.io/eyeprolog/examples/deck/scientific-evidence-graph). Together they cover policy decisions, reversible configuration reasoning, dependency-graph diagnosis, transitive SBOM exposure, and evidence aggregation with explicit disagreement.
|
|
85
85
|
|
|
86
86
|
## Benchmarks
|
|
87
|
-
EyeProlog has
|
|
87
|
+
EyeProlog has 21 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, rewriting, the dynamic database, and bignum arithmetic. Short workloads are adaptively batched before timing so millisecond-scale noise is not mistaken for a regression. Run `npm run benchmark`; create a machine-local comparison point with `npm run benchmark -- --save .benchmarks/baseline.json`; use `node test/run-benchmark-tests.mjs` for harness checks. For a classic LIPS number, run `node test/lips-benchmark.mjs`: it executes the classic failure-driven `dobench/1` and `dodummy/1` loops in Prolog over the checked [`examples/bench.pl`](examples/bench.pl) naive-reverse workload (the classic Quintus 1984 `nrev/2` benchmark on a 30-element list), subtracts dummy-loop CPU time, and applies the historical 496 procedure calls per reversal. LIPS is a historical basic-engine-speed indicator, not a whole-system performance score. Details are in [*The Art of EyeProlog*](the-art-of-eyeprolog.md).
|
|
88
88
|
For the project policy on post-ISO-standard and WG17 compatibility features such as digit separators, see [ISO/WG17 compatibility extensions](test/conformance/ISO-WG17-EXTENSIONS.md).
|
|
89
89
|
## Development
|
|
90
90
|
```sh
|
package/package.json
CHANGED
package/src/iso.js
CHANGED
|
@@ -1162,6 +1162,12 @@ function streamHandleId(value, env) {
|
|
|
1162
1162
|
// and rejecting an equally valid stream reference as malformed.
|
|
1163
1163
|
if (value.type !== COMPOUND || value.name !== '$stream' || value.arity !== 1) return null;
|
|
1164
1164
|
const id = deref(value.args[0], env);
|
|
1165
|
+
// The shape is right, but the argument that would identify the stream is
|
|
1166
|
+
// still unbound: whether this term is a valid stream reference cannot be
|
|
1167
|
+
// decided yet, so that is an instantiation error, not a domain error (see
|
|
1168
|
+
// issue #109's stc#72 follow-up) — unlike a wrong functor/arity above,
|
|
1169
|
+
// which can never become valid no matter how any variable gets bound.
|
|
1170
|
+
if (id.type === VAR) throw new PrologError('instantiation_error');
|
|
1165
1171
|
return id.type === NUMBER && isDecimalInteger(id.name) ? Number(id.name) : null;
|
|
1166
1172
|
}
|
|
1167
1173
|
|
|
@@ -8,16 +8,6 @@
|
|
|
8
8
|
],
|
|
9
9
|
"expectedSha256": "a0aa24fcbc87c394a4da789ac861b8e18cabad5e0d73b25ddc02df5dd4376c1b"
|
|
10
10
|
},
|
|
11
|
-
{
|
|
12
|
-
"name": "classic-nrev",
|
|
13
|
-
"group": "recursion/lips",
|
|
14
|
-
"file": "examples/bench.pl",
|
|
15
|
-
"goals": [
|
|
16
|
-
"bench_result(X0)"
|
|
17
|
-
],
|
|
18
|
-
"expectedSha256": "8045dc0730e4996a7b01fb2348311fb175ed53f4c49d14f82e01a3d470602255",
|
|
19
|
-
"logicalInferences": 496
|
|
20
|
-
},
|
|
21
11
|
{
|
|
22
12
|
"name": "clpz-sudoku",
|
|
23
13
|
"group": "clpz",
|
|
@@ -172,5 +162,32 @@
|
|
|
172
162
|
"bulk_write_result(X0, X1)"
|
|
173
163
|
],
|
|
174
164
|
"expectedSha256": "8471da130a4a3a92dd31ee6cf285b1e129886177d7be367d51650a48b8920324"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "quine-mccluskey",
|
|
168
|
+
"group": "dynamic-database",
|
|
169
|
+
"file": "examples/quine-mccluskey.pl",
|
|
170
|
+
"goals": [
|
|
171
|
+
"answer(X0)"
|
|
172
|
+
],
|
|
173
|
+
"expectedSha256": "9ebf4a55f6f15a78ad614da02b418650d77cae39eb0d49cbf9eb614865bec501"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "fibonacci",
|
|
177
|
+
"group": "tabling",
|
|
178
|
+
"file": "examples/fibonacci.pl",
|
|
179
|
+
"goals": [
|
|
180
|
+
"fibonacci(X0, X1)"
|
|
181
|
+
],
|
|
182
|
+
"expectedSha256": "3adc819083c11d6bf4195e2718904d1712c21553d8fcad970c11a4a4e79e2a3f"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"name": "iso-atomic-conversion",
|
|
186
|
+
"group": "term-io",
|
|
187
|
+
"file": "examples/iso-atomic-conversion.pl",
|
|
188
|
+
"goals": [
|
|
189
|
+
"report(X0, X1)"
|
|
190
|
+
],
|
|
191
|
+
"expectedSha256": "68c82dac7dd666cae5af51786f4fbc75423aac12309cedda1e342aba08ea4253"
|
|
175
192
|
}
|
|
176
193
|
]
|
package/test/benchmark.mjs
CHANGED
|
@@ -69,9 +69,6 @@ for (const item of manifest) {
|
|
|
69
69
|
if (!/^[0-9a-f]{64}$/.test(item.expectedSha256)) {
|
|
70
70
|
throw new Error(`invalid expectedSha256 for benchmark ${item.name}`);
|
|
71
71
|
}
|
|
72
|
-
if (item.logicalInferences != null && (!Number.isInteger(item.logicalInferences) || item.logicalInferences <= 0)) {
|
|
73
|
-
throw new Error(`invalid logicalInferences for benchmark ${item.name}`);
|
|
74
|
-
}
|
|
75
72
|
}
|
|
76
73
|
|
|
77
74
|
const selected = manifest.filter((item) => {
|
|
@@ -118,16 +115,6 @@ function formatMs(value) {
|
|
|
118
115
|
return `${value.toFixed(1)} ms`;
|
|
119
116
|
}
|
|
120
117
|
|
|
121
|
-
function lipsFor(logicalInferences, medianMs) {
|
|
122
|
-
if (!Number.isFinite(logicalInferences) || !Number.isFinite(medianMs) || medianMs <= 0) return null;
|
|
123
|
-
return logicalInferences * 1000 / medianMs;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function formatLips(value) {
|
|
127
|
-
if (value == null) return '—';
|
|
128
|
-
return Math.round(value).toLocaleString('en-US');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
118
|
function changePercent(medianMs, baselineMs) {
|
|
132
119
|
if (baselineMs == null || baselineMs === 0) return null;
|
|
133
120
|
return ((medianMs - baselineMs) / baselineMs) * 100;
|
|
@@ -141,6 +128,28 @@ function changeText(item) {
|
|
|
141
128
|
return `${change < 0 ? '↓' : '↑'} ${value}`;
|
|
142
129
|
}
|
|
143
130
|
|
|
131
|
+
// A regression (slower) reads red, an improvement (faster) reads green, and a
|
|
132
|
+
// change too small to trust (< 5%) reads dim rather than either color, so the
|
|
133
|
+
// palette itself does not editorialize about noise-level swings.
|
|
134
|
+
function changeColor(item) {
|
|
135
|
+
if (item.changePercent == null) return null;
|
|
136
|
+
if (Math.abs(item.changePercent) < 5) return 'dim';
|
|
137
|
+
return item.changePercent < 0 ? 'green' : 'red';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const supportsColor = !options.json && process.stdout.isTTY &&
|
|
141
|
+
process.env.NO_COLOR == null && process.env.TERM !== 'dumb';
|
|
142
|
+
const ANSI = { bold: '1', dim: '2', green: '32', red: '31' };
|
|
143
|
+
function paint(name, text) {
|
|
144
|
+
return name && supportsColor ? `\x1b[${ANSI[name]}m${text}\x1b[0m` : text;
|
|
145
|
+
}
|
|
146
|
+
function visibleLength(text) {
|
|
147
|
+
return text.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
148
|
+
}
|
|
149
|
+
function padVisible(text, width) {
|
|
150
|
+
return text + ' '.repeat(Math.max(0, width - visibleLength(text)));
|
|
151
|
+
}
|
|
152
|
+
|
|
144
153
|
function summarizeResults(results) {
|
|
145
154
|
const comparable = results.filter((item) =>
|
|
146
155
|
Number.isFinite(item.medianMs) &&
|
|
@@ -226,8 +235,6 @@ for (const item of selected) {
|
|
|
226
235
|
answerLines: worker.answerLines,
|
|
227
236
|
outputBytes: worker.outputBytes,
|
|
228
237
|
sha256: worker.digest,
|
|
229
|
-
logicalInferences: item.logicalInferences ?? null,
|
|
230
|
-
lips: lipsFor(item.logicalInferences, medianMs),
|
|
231
238
|
baselineMs: baselineItem?.medianMs ?? null,
|
|
232
239
|
changePercent: changePercent(medianMs, baselineItem?.medianMs ?? null),
|
|
233
240
|
});
|
|
@@ -265,42 +272,36 @@ if (options.json) {
|
|
|
265
272
|
results,
|
|
266
273
|
}, null, 2)}\n`);
|
|
267
274
|
} else {
|
|
268
|
-
const headers = ['Benchmark', 'Median/op', '
|
|
275
|
+
const headers = ['Benchmark', 'Median/op', 'Range/op', 'Batch', 'Baseline', 'Change', 'Answers'];
|
|
269
276
|
const rows = results.map((item) => [
|
|
270
277
|
item.name,
|
|
271
278
|
formatMs(item.medianMs),
|
|
272
|
-
formatLips(item.lips),
|
|
273
279
|
`${formatMs(item.minMs)}–${formatMs(item.maxMs)}`,
|
|
274
280
|
String(item.batchSize),
|
|
275
281
|
formatMs(item.baselineMs),
|
|
276
|
-
changeText(item),
|
|
282
|
+
paint(changeColor(item), changeText(item)),
|
|
277
283
|
String(item.answerLines),
|
|
278
284
|
]);
|
|
279
|
-
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]
|
|
280
|
-
const printRow = (row) => process.stdout.write(
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => visibleLength(row[index]))));
|
|
286
|
+
const printRow = (row, style) => process.stdout.write(
|
|
287
|
+
`${row.map((cell, index) => paint(style, padVisible(cell, widths[index]))).join(' ')}\n`,
|
|
288
|
+
);
|
|
289
|
+
printRow(headers, 'bold');
|
|
290
|
+
printRow(widths.map((width) => '-'.repeat(width)), 'dim');
|
|
283
291
|
for (const row of rows) printRow(row);
|
|
284
292
|
|
|
285
293
|
if (summary.comparable > 0) {
|
|
286
294
|
process.stdout.write(
|
|
287
295
|
`\nSuite score: ${summary.ratio.toFixed(3)}x baseline ` +
|
|
288
|
-
`(${changeText({ changePercent: summary.changePercent })})
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`Time-weighted total: ${formatMs(summary.currentTotalMs)} vs ${formatMs(summary.baselineTotalMs)} ` +
|
|
293
|
-
`(${changeText({ changePercent: summary.totalChangePercent })}).\n`,
|
|
296
|
+
`(${paint(changeColor({ changePercent: summary.changePercent }), changeText({ changePercent: summary.changePercent }))}), ` +
|
|
297
|
+
`${summary.comparable}/${summary.total} comparable. ` +
|
|
298
|
+
`Time-weighted: ${formatMs(summary.currentTotalMs)} vs ${formatMs(summary.baselineTotalMs)} ` +
|
|
299
|
+
`(${paint(changeColor({ changePercent: summary.totalChangePercent }), changeText({ changePercent: summary.totalChangePercent }))}).\n`,
|
|
294
300
|
);
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
process.stdout.write(`\n${results.length} benchmarks
|
|
298
|
-
if (baselineWarning) process.stdout.write(`${baselineWarning}\n`);
|
|
299
|
-
if (baselinePath == null
|
|
300
|
-
if (options.save != null) process.stdout.write(`Saved
|
|
301
|
-
process.stdout.write('Change compares the current median/op directly with the saved baseline median/op; the measured range is shown separately.\n');
|
|
302
|
-
process.stdout.write('The classic-nrev LIPS column is a quick wall-clock estimate. Use node test/lips-benchmark.mjs for the Quintus-style dummy-subtracted CPU measurement.\n');
|
|
303
|
-
if (summary.comparable > 0) {
|
|
304
|
-
process.stdout.write('Suite score is the geometric mean of current/baseline ratios, so every benchmark has equal relative weight; Time-weighted total compares summed medians and is dominated by longer workloads.\n');
|
|
305
|
-
}
|
|
303
|
+
process.stdout.write(`\n${results.length} benchmarks, ${options.runs}× after ${options.warmup}× warm-up, ~${options.targetMs} ms/batch.\n`);
|
|
304
|
+
if (baselineWarning) process.stdout.write(paint('dim', `${baselineWarning}\n`));
|
|
305
|
+
else if (baselinePath == null) process.stdout.write(paint('dim', 'No baseline: npm run benchmark -- --save .benchmarks/baseline.json\n'));
|
|
306
|
+
if (options.save != null) process.stdout.write(`Saved baseline: ${path.relative(root, options.save)}\n`);
|
|
306
307
|
}
|
|
@@ -46,6 +46,21 @@ export function regressionCases() {
|
|
|
46
46
|
assertIncludes(result.stdout, 'A1 = ', 'query succeeds and reports the shared stream number');
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
|
+
{
|
|
50
|
+
name: 'close/1 raises instantiation_error, not domain_error(stream_or_alias), for a $stream/1 term whose argument is unbound (issue #109 follow-up)',
|
|
51
|
+
run: () => {
|
|
52
|
+
const result = runCli([], { input:
|
|
53
|
+
"S='$stream'(X),close(S).\nhalt.\n",
|
|
54
|
+
});
|
|
55
|
+
assertEqual(result.status, 0, result.stderr);
|
|
56
|
+
// The argument that would identify the stream is still unbound, so
|
|
57
|
+
// whether this term is a valid stream reference cannot be decided
|
|
58
|
+
// yet — that calls for instantiation_error, not a domain_error
|
|
59
|
+
// claiming the term itself is already known to be invalid.
|
|
60
|
+
assertIncludes(result.stdout, 'error(instantiation_error,', 'unbound $stream/1 argument reports instantiation_error');
|
|
61
|
+
assertNotIncludes(result.stdout, 'domain_error(stream_or_alias', 'no domain_error(stream_or_alias, ...) once the argument is still unbound');
|
|
62
|
+
},
|
|
63
|
+
},
|
|
49
64
|
{
|
|
50
65
|
name: 'top level never mints a generated variable name that collides with a query variable\'s own name (issue #108)',
|
|
51
66
|
run: () => {
|
|
@@ -42,10 +42,8 @@ function runWorker(item) {
|
|
|
42
42
|
]);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
ok(Array.isArray(manifest) && manifest.length ===
|
|
45
|
+
ok(Array.isArray(manifest) && manifest.length === 21, 'benchmark manifest should contain exactly 21 representative workloads');
|
|
46
46
|
ok(new Set(manifest.map((item) => item.name)).size === manifest.length, 'benchmark names should be unique');
|
|
47
|
-
const classicNrev = manifest.find((item) => item.name === 'classic-nrev');
|
|
48
|
-
ok(classicNrev?.logicalInferences === 496, 'classic nrev should retain the traditional 496-call LIPS accounting');
|
|
49
47
|
|
|
50
48
|
for (const item of manifest) {
|
|
51
49
|
ok(/^[0-9a-f]{64}$/.test(item.expectedSha256), `${item.name} should have a committed semantic checksum`);
|
|
@@ -72,18 +70,6 @@ ok(adaptive.results[0].batchSize > 1, 'adaptive benchmark smoke test should batc
|
|
|
72
70
|
ok(adaptive.results[0].sha256 === manifest.find((item) => item.name === 'dcg-expression').expectedSha256,
|
|
73
71
|
'adaptive batching should preserve the semantic checksum');
|
|
74
72
|
|
|
75
|
-
const nrev = await spawnJson([
|
|
76
|
-
path.join(root, 'test', 'benchmark.mjs'),
|
|
77
|
-
'--filter', 'classic-nrev',
|
|
78
|
-
'--runs', '1',
|
|
79
|
-
'--warmup', '0',
|
|
80
|
-
'--target-ms', '0',
|
|
81
|
-
'--json',
|
|
82
|
-
]);
|
|
83
|
-
ok(nrev.results.length === 1, 'classic nrev benchmark should be selectable');
|
|
84
|
-
ok(nrev.results[0].logicalInferences === 496, 'classic nrev JSON should expose its logical-inference count');
|
|
85
|
-
ok(Number.isFinite(nrev.results[0].lips) && nrev.results[0].lips > 0, 'classic nrev JSON should report positive LIPS');
|
|
86
|
-
|
|
87
73
|
const classicLips = await spawnJson([
|
|
88
74
|
path.join(root, 'test', 'lips-benchmark.mjs'),
|
|
89
75
|
'--count', '20',
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -10358,7 +10358,7 @@ npm run benchmark -- --save .benchmarks/baseline.json
|
|
|
10358
10358
|
node test/lips-benchmark.mjs
|
|
10359
10359
|
```
|
|
10360
10360
|
|
|
10361
|
-
The benchmark suite contains
|
|
10361
|
+
The benchmark suite contains 21 representative workloads and stores their
|
|
10362
10362
|
semantic output digests in the repository, while wall-clock baselines remain
|
|
10363
10363
|
machine-local under `.benchmarks/` because absolute timings are machine-specific.
|
|
10364
10364
|
Each benchmark runs in its own fresh Node worker. Inside that worker, one untimed
|