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 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 19 checksum-protected wall-clock benchmarks spanning recursion/indexing, constraints, tabling/WFS, DCGs, Eyelet, search, term I/O, attributes, rewriting, and the classic Prolog naive-reverse workload. 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. The checked [`examples/bench.pl`](examples/bench.pl) preserves the classic Quintus 1984 `nrev/2` workload on a 30-element list. For a comparable LIPS number, run `node test/lips-benchmark.mjs`: it executes the classic failure-driven `dobench/1` and `dodummy/1` loops in Prolog, subtracts dummy-loop CPU time, and applies the historical 496 procedure calls per reversal. The generic benchmark table still shows a quick wall-clock LIPS estimate for `classic-nrev`, but `node test/lips-benchmark.mjs` is the canonical engine-speed measurement. 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).
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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.76",
6
+ "version": "1.5.78",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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
  ]
@@ -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', 'LIPS', 'Range/op', 'Batch', 'Baseline', 'Change', 'Answers'];
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].length)));
280
- const printRow = (row) => process.stdout.write(`${row.map((cell, index) => cell.padEnd(widths[index])).join(' ')}\n`);
281
- printRow(headers);
282
- printRow(widths.map((width) => '-'.repeat(width)));
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
- `equal-weight geometric mean across ${summary.comparable}/${summary.total} comparable benchmarks.\n`,
290
- );
291
- process.stdout.write(
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; ${options.runs} measured batch${options.runs === 1 ? '' : 'es'} each after ${options.warmup} warm-up batch${options.warmup === 1 ? '' : 'es'}, calibrated after one priming execution toward ${options.targetMs} ms per batch.\n`);
298
- if (baselineWarning) process.stdout.write(`${baselineWarning}\n`);
299
- if (baselinePath == null && !baselineWarning) process.stdout.write('No timing baseline found; run npm run benchmark -- --save .benchmarks/baseline.json to create .benchmarks/baseline.json.\n');
300
- if (options.save != null) process.stdout.write(`Saved timing baseline: ${path.relative(root, options.save)}\n`);
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 === 19, 'benchmark manifest should contain exactly 19 representative workloads');
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',
@@ -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 19 representative workloads and stores their
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