eyeprolog 1.3.63 → 1.3.65

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.63",
6
+ "version": "1.3.65",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/test/run-all.mjs CHANGED
@@ -23,6 +23,6 @@ await runStandalone(async (reporter) => {
23
23
  runCleanup(reporter);
24
24
  runRegression(reporter);
25
25
  await runPlayground(reporter);
26
- runExamples(reporter);
26
+ await runExamples(reporter);
27
27
  runBookExamples(reporter);
28
28
  });
@@ -2,8 +2,10 @@
2
2
  // Example-output test runner.
3
3
  // It compares examples byte-for-byte against golden output so answer and proof changes cannot silently alter results.
4
4
  import fs from 'node:fs';
5
+ import os from 'node:os';
5
6
  import path from 'node:path';
6
7
  import { spawnSync } from 'node:child_process';
8
+ import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
7
9
  import { Program, run } from '../src/index.js';
8
10
  import { fileURLToPath } from 'node:url';
9
11
  import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
@@ -79,13 +81,19 @@ export const proofExamples = [
79
81
  'partial-evaluator.pl',
80
82
  ];
81
83
 
82
- export function runExamples(reporter = new TestReporter()) {
84
+ export async function runExamples(reporter = new TestReporter()) {
83
85
  const files = fs.readdirSync(examplesDir)
84
86
  .filter((name) => exampleIsRunnable(name))
85
87
  .sort();
86
88
 
87
89
  reporter.section('Examples');
88
- for (const name of files) reporter.test(name, () => runExample(name));
90
+ const deepTaxonomyIndex = files.indexOf('deep-taxonomy-100000.pl');
91
+ if (deepTaxonomyIndex >= 0) {
92
+ await runExampleTasks(files.slice(0, deepTaxonomyIndex + 1), (name, result) => reporter.testResult(name, result), 2);
93
+ await runExampleTasks(files.slice(deepTaxonomyIndex + 1), (name, result) => reporter.testResult(name, result), 3);
94
+ } else {
95
+ await runExampleTasks(files, (name, result) => reporter.testResult(name, result), 3);
96
+ }
89
97
  reporter.sectionTotal('examples');
90
98
 
91
99
  reporter.section('Proof examples');
@@ -94,6 +102,101 @@ export function runExamples(reporter = new TestReporter()) {
94
102
  }
95
103
 
96
104
 
105
+ async function runExampleTasks(tasks, onResult, maxWorkers) {
106
+ if (tasks.length === 0) return;
107
+ const parallelism = os.availableParallelism?.() ?? os.cpus().length;
108
+ const workerCount = Math.min(tasks.length, Math.max(1, Math.min(maxWorkers, parallelism - 1)));
109
+ if (workerCount === 1) {
110
+ for (const name of tasks) {
111
+ const startedAt = performance.now();
112
+ try {
113
+ runExample(name);
114
+ onResult(name, { ms: Math.round(performance.now() - startedAt) });
115
+ } catch (error) {
116
+ onResult(name, { ms: Math.round(performance.now() - startedAt), error });
117
+ }
118
+ }
119
+ return;
120
+ }
121
+
122
+ const completedResults = new Map();
123
+ let nextTask = 0;
124
+ let nextReport = 0;
125
+ let completed = 0;
126
+ let settled = false;
127
+
128
+ await new Promise((resolve, reject) => {
129
+ const workers = Array.from({ length: workerCount }, () => new Worker(new URL(import.meta.url), {
130
+ workerData: { exampleWorker: true },
131
+ }));
132
+
133
+ const stopWorkers = () => Promise.all(workers.map((worker) => worker.terminate()));
134
+
135
+ const fail = (error) => {
136
+ if (settled) return;
137
+ settled = true;
138
+ stopWorkers().finally(() => reject(error));
139
+ };
140
+
141
+ const finish = () => {
142
+ if (settled || completed !== tasks.length || nextReport !== tasks.length) return;
143
+ settled = true;
144
+ stopWorkers().then(() => resolve(), reject);
145
+ };
146
+
147
+ const reportReady = () => {
148
+ try {
149
+ while (completedResults.has(nextReport)) {
150
+ const result = completedResults.get(nextReport);
151
+ completedResults.delete(nextReport);
152
+ onResult(tasks[nextReport], result);
153
+ nextReport++;
154
+ }
155
+ } catch (error) {
156
+ fail(error);
157
+ }
158
+ };
159
+
160
+ const assign = (worker) => {
161
+ if (nextTask >= tasks.length || settled) return;
162
+ worker.postMessage({ id: nextTask, name: tasks[nextTask++] });
163
+ };
164
+
165
+ for (const worker of workers) {
166
+ worker.on('message', ({ id, ms, error }) => {
167
+ if (settled) return;
168
+ completedResults.set(id, {
169
+ ms,
170
+ error: error == null ? null : Object.assign(new Error(error.message), { stack: error.stack }),
171
+ });
172
+ completed++;
173
+ reportReady();
174
+ assign(worker);
175
+ finish();
176
+ });
177
+ worker.on('error', fail);
178
+ assign(worker);
179
+ }
180
+ });
181
+ }
182
+
183
+ function runExampleWorker() {
184
+ parentPort.on('message', ({ id, name }) => {
185
+ const startedAt = performance.now();
186
+ try {
187
+ runExample(name);
188
+ parentPort.postMessage({ id, ms: Math.round(performance.now() - startedAt), error: null });
189
+ } catch (error) {
190
+ parentPort.postMessage({
191
+ id,
192
+ ms: Math.round(performance.now() - startedAt),
193
+ error: { message: error?.message ?? String(error), stack: error?.stack ?? String(error) },
194
+ });
195
+ }
196
+ });
197
+ }
198
+
199
+
97
200
  function exampleIsRunnable(name) {
98
201
  return name.endsWith('.pl');
99
202
  }
@@ -155,6 +258,8 @@ function diffText(expected, actualText) {
155
258
  return 'outputs differ';
156
259
  }
157
260
 
158
- if (isMainModule(import.meta.url)) {
261
+ if (!isMainThread && workerData?.exampleWorker) {
262
+ runExampleWorker();
263
+ } else if (isMainModule(import.meta.url)) {
159
264
  await runStandalone(runExamples);
160
265
  }
@@ -87,6 +87,21 @@ export class TestReporter {
87
87
  }
88
88
  }
89
89
 
90
+ testResult(name, { ms = 0, error = null } = {}) {
91
+ this.total++;
92
+ const nr = String(this.total).padStart(3, '0');
93
+
94
+ if (error == null) {
95
+ this.ok++;
96
+ this.stdout.write(`${colors.green}OK${colors.reset} ${nr} ${name} ${colors.dim}(${ms} ms)${colors.reset}\n`);
97
+ return;
98
+ }
99
+
100
+ this.stderr.write(`${colors.red}FAIL${colors.reset} ${nr} ${name} ${colors.dim}(${ms} ms)${colors.reset}\n`);
101
+ this.stderr.write(`${error?.stack ?? String(error)}\n`);
102
+ throw error;
103
+ }
104
+
90
105
  totalLine() {
91
106
  const ms = nowMs() - this.startedAt;
92
107
  this.stdout.write(`\n${colors.yellow}== Total${colors.reset}\n`);