eyeprolog 1.3.63 → 1.3.64

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.64",
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,13 @@ 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
+ await runExampleTasks(files, (name, result) => reporter.testResult(name, result));
89
91
  reporter.sectionTotal('examples');
90
92
 
91
93
  reporter.section('Proof examples');
@@ -94,6 +96,101 @@ export function runExamples(reporter = new TestReporter()) {
94
96
  }
95
97
 
96
98
 
99
+ async function runExampleTasks(tasks, onResult) {
100
+ if (tasks.length === 0) return;
101
+ const parallelism = os.availableParallelism?.() ?? os.cpus().length;
102
+ const workerCount = Math.min(tasks.length, Math.max(1, Math.min(3, parallelism - 1)));
103
+ if (workerCount === 1) {
104
+ for (const name of tasks) {
105
+ const startedAt = performance.now();
106
+ try {
107
+ runExample(name);
108
+ onResult(name, { ms: Math.round(performance.now() - startedAt) });
109
+ } catch (error) {
110
+ onResult(name, { ms: Math.round(performance.now() - startedAt), error });
111
+ }
112
+ }
113
+ return;
114
+ }
115
+
116
+ const completedResults = new Map();
117
+ let nextTask = 0;
118
+ let nextReport = 0;
119
+ let completed = 0;
120
+ let settled = false;
121
+
122
+ await new Promise((resolve, reject) => {
123
+ const workers = Array.from({ length: workerCount }, () => new Worker(new URL(import.meta.url), {
124
+ workerData: { exampleWorker: true },
125
+ }));
126
+
127
+ const stopWorkers = () => Promise.all(workers.map((worker) => worker.terminate()));
128
+
129
+ const fail = (error) => {
130
+ if (settled) return;
131
+ settled = true;
132
+ stopWorkers().finally(() => reject(error));
133
+ };
134
+
135
+ const finish = () => {
136
+ if (settled || completed !== tasks.length || nextReport !== tasks.length) return;
137
+ settled = true;
138
+ stopWorkers().then(() => resolve(), reject);
139
+ };
140
+
141
+ const reportReady = () => {
142
+ try {
143
+ while (completedResults.has(nextReport)) {
144
+ const result = completedResults.get(nextReport);
145
+ completedResults.delete(nextReport);
146
+ onResult(tasks[nextReport], result);
147
+ nextReport++;
148
+ }
149
+ } catch (error) {
150
+ fail(error);
151
+ }
152
+ };
153
+
154
+ const assign = (worker) => {
155
+ if (nextTask >= tasks.length || settled) return;
156
+ worker.postMessage({ id: nextTask, name: tasks[nextTask++] });
157
+ };
158
+
159
+ for (const worker of workers) {
160
+ worker.on('message', ({ id, ms, error }) => {
161
+ if (settled) return;
162
+ completedResults.set(id, {
163
+ ms,
164
+ error: error == null ? null : Object.assign(new Error(error.message), { stack: error.stack }),
165
+ });
166
+ completed++;
167
+ reportReady();
168
+ assign(worker);
169
+ finish();
170
+ });
171
+ worker.on('error', fail);
172
+ assign(worker);
173
+ }
174
+ });
175
+ }
176
+
177
+ function runExampleWorker() {
178
+ parentPort.on('message', ({ id, name }) => {
179
+ const startedAt = performance.now();
180
+ try {
181
+ runExample(name);
182
+ parentPort.postMessage({ id, ms: Math.round(performance.now() - startedAt), error: null });
183
+ } catch (error) {
184
+ parentPort.postMessage({
185
+ id,
186
+ ms: Math.round(performance.now() - startedAt),
187
+ error: { message: error?.message ?? String(error), stack: error?.stack ?? String(error) },
188
+ });
189
+ }
190
+ });
191
+ }
192
+
193
+
97
194
  function exampleIsRunnable(name) {
98
195
  return name.endsWith('.pl');
99
196
  }
@@ -155,6 +252,8 @@ function diffText(expected, actualText) {
155
252
  return 'outputs differ';
156
253
  }
157
254
 
158
- if (isMainModule(import.meta.url)) {
255
+ if (!isMainThread && workerData?.exampleWorker) {
256
+ runExampleWorker();
257
+ } else if (isMainModule(import.meta.url)) {
159
258
  await runStandalone(runExamples);
160
259
  }
@@ -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`);