eyeprolog 1.6.29 → 1.6.30

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.6.29",
6
+ "version": "1.6.30",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -0,0 +1,113 @@
1
+ // A small ordered worker pool for the suites that do the same work once per
2
+ // file: run every example, prove every example, check every packaged proof.
3
+ //
4
+ // Results are reported in task order however the workers finish, so a parallel
5
+ // suite numbers its tests exactly as the sequential one did and a failure still
6
+ // stops the run at the same test. Each suite compares inside its worker, so
7
+ // only a name, an elapsed time, an optional failure, and a small plain-data
8
+ // value cross the thread boundary.
9
+ import os from 'node:os';
10
+ import { Worker } from 'node:worker_threads';
11
+
12
+ export async function runTasksInParallel({
13
+ names,
14
+ workerUrl,
15
+ workerData,
16
+ message = {},
17
+ maxWorkers,
18
+ runLocally,
19
+ onResult,
20
+ }) {
21
+ if (names.length === 0) return;
22
+ const parallelism = os.availableParallelism?.() ?? os.cpus().length;
23
+ const workerCount = Math.min(names.length, Math.max(1, Math.min(maxWorkers, parallelism - 1)));
24
+ if (workerCount === 1) {
25
+ for (const name of names) {
26
+ const startedAt = performance.now();
27
+ try {
28
+ const value = runLocally(name) ?? null;
29
+ onResult(name, { ms: Math.round(performance.now() - startedAt), value });
30
+ } catch (error) {
31
+ onResult(name, { ms: Math.round(performance.now() - startedAt), error });
32
+ }
33
+ }
34
+ return;
35
+ }
36
+
37
+ const completedResults = new Map();
38
+ let nextTask = 0;
39
+ let nextReport = 0;
40
+ let completed = 0;
41
+ let settled = false;
42
+
43
+ await new Promise((resolve, reject) => {
44
+ const workers = Array.from({ length: workerCount }, () => new Worker(workerUrl, { workerData }));
45
+
46
+ const stopWorkers = () => Promise.all(workers.map((worker) => worker.terminate()));
47
+
48
+ const fail = (error) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ stopWorkers().finally(() => reject(error));
52
+ };
53
+
54
+ const finish = () => {
55
+ if (settled || completed !== names.length || nextReport !== names.length) return;
56
+ settled = true;
57
+ stopWorkers().then(() => resolve(), reject);
58
+ };
59
+
60
+ const reportReady = () => {
61
+ try {
62
+ while (completedResults.has(nextReport)) {
63
+ const result = completedResults.get(nextReport);
64
+ completedResults.delete(nextReport);
65
+ onResult(names[nextReport], result);
66
+ nextReport++;
67
+ }
68
+ } catch (error) {
69
+ fail(error);
70
+ }
71
+ };
72
+
73
+ const assign = (worker) => {
74
+ if (nextTask >= names.length || settled) return;
75
+ worker.postMessage({ ...message, id: nextTask, name: names[nextTask++] });
76
+ };
77
+
78
+ for (const worker of workers) {
79
+ worker.on('message', ({ id, ms, error, value }) => {
80
+ if (settled) return;
81
+ completedResults.set(id, {
82
+ ms,
83
+ value: value ?? null,
84
+ error: error == null ? null : Object.assign(new Error(error.message), { stack: error.stack }),
85
+ });
86
+ completed++;
87
+ reportReady();
88
+ assign(worker);
89
+ finish();
90
+ });
91
+ worker.on('error', fail);
92
+ assign(worker);
93
+ }
94
+ });
95
+ }
96
+
97
+ // The worker side of the same protocol. `runTask` receives the posted message
98
+ // and may return a plain-data value for the pool to hand back with the result.
99
+ export function serveTasks(parentPort, runTask) {
100
+ parentPort.on('message', (task) => {
101
+ const startedAt = performance.now();
102
+ try {
103
+ const value = runTask(task) ?? null;
104
+ parentPort.postMessage({ id: task.id, ms: Math.round(performance.now() - startedAt), error: null, value });
105
+ } catch (error) {
106
+ parentPort.postMessage({
107
+ id: task.id,
108
+ ms: Math.round(performance.now() - startedAt),
109
+ error: { message: error?.message ?? String(error), stack: error?.stack ?? String(error) },
110
+ });
111
+ }
112
+ });
113
+ }
package/test/run-all.mjs CHANGED
@@ -35,6 +35,6 @@ await runStandalone(async (reporter) => {
35
35
  await runRegression(reporter);
36
36
  await runPlayground(reporter);
37
37
  await runExamples(reporter);
38
- runProofChecking(reporter);
38
+ await runProofChecking(reporter);
39
39
  runBookExamples(reporter);
40
40
  });
@@ -2,13 +2,13 @@
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';
6
5
  import path from 'node:path';
7
6
  import { spawnSync } from 'node:child_process';
8
- import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
7
+ import { isMainThread, parentPort, workerData } from 'node:worker_threads';
9
8
  import { Program, run } from '../src/index.js';
10
9
  import { fileURLToPath } from 'node:url';
11
10
  import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
11
+ import { runTasksInParallel, serveTasks } from './parallel-tasks.mjs';
12
12
  import { goalsInProgramOrder } from './goal-metadata.mjs';
13
13
 
14
14
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
@@ -31,107 +31,39 @@ export async function runExamples(reporter = new TestReporter()) {
31
31
  .sort();
32
32
 
33
33
  reporter.section('Examples');
34
- await runExampleTasks(files, (name, result) => reporter.testResult(name, result), 3);
34
+ await runExampleTasks(files, 'output', reporter);
35
35
  reporter.sectionTotal('examples');
36
36
 
37
+ // Proving an example costs several times what running it does, so this is
38
+ // the longer of the two passes over the same corpus. It is the same work per
39
+ // program and the same isolation, so it goes through the same worker pool.
37
40
  reporter.section('Proof examples');
38
- for (const name of proofExamples) reporter.test(name, () => runProofExample(name));
41
+ await runExampleTasks(proofExamples, 'proof', reporter);
39
42
  reporter.sectionTotal('proof examples');
40
43
  }
41
44
 
42
-
43
- async function runExampleTasks(tasks, onResult, maxWorkers) {
44
- if (tasks.length === 0) return;
45
- const parallelism = os.availableParallelism?.() ?? os.cpus().length;
46
- const workerCount = Math.min(tasks.length, Math.max(1, Math.min(maxWorkers, parallelism - 1)));
47
- if (workerCount === 1) {
48
- for (const name of tasks) {
49
- const startedAt = performance.now();
50
- try {
51
- runExample(name);
52
- onResult(name, { ms: Math.round(performance.now() - startedAt) });
53
- } catch (error) {
54
- onResult(name, { ms: Math.round(performance.now() - startedAt), error });
55
- }
56
- }
57
- return;
58
- }
59
-
60
- const completedResults = new Map();
61
- let nextTask = 0;
62
- let nextReport = 0;
63
- let completed = 0;
64
- let settled = false;
65
-
66
- await new Promise((resolve, reject) => {
67
- const workers = Array.from({ length: workerCount }, () => new Worker(new URL(import.meta.url), {
68
- workerData: { exampleWorker: true },
69
- }));
70
-
71
- const stopWorkers = () => Promise.all(workers.map((worker) => worker.terminate()));
72
-
73
- const fail = (error) => {
74
- if (settled) return;
75
- settled = true;
76
- stopWorkers().finally(() => reject(error));
77
- };
78
-
79
- const finish = () => {
80
- if (settled || completed !== tasks.length || nextReport !== tasks.length) return;
81
- settled = true;
82
- stopWorkers().then(() => resolve(), reject);
83
- };
84
-
85
- const reportReady = () => {
86
- try {
87
- while (completedResults.has(nextReport)) {
88
- const result = completedResults.get(nextReport);
89
- completedResults.delete(nextReport);
90
- onResult(tasks[nextReport], result);
91
- nextReport++;
92
- }
93
- } catch (error) {
94
- fail(error);
95
- }
96
- };
97
-
98
- const assign = (worker) => {
99
- if (nextTask >= tasks.length || settled) return;
100
- worker.postMessage({ id: nextTask, name: tasks[nextTask++] });
101
- };
102
-
103
- for (const worker of workers) {
104
- worker.on('message', ({ id, ms, error }) => {
105
- if (settled) return;
106
- completedResults.set(id, {
107
- ms,
108
- error: error == null ? null : Object.assign(new Error(error.message), { stack: error.stack }),
109
- });
110
- completed++;
111
- reportReady();
112
- assign(worker);
113
- finish();
114
- });
115
- worker.on('error', fail);
116
- assign(worker);
117
- }
45
+ // What a worker is asked to do with one example. Results are compared inside
46
+ // the worker, so only a name, an elapsed time, and a failure cross the thread.
47
+ const exampleTaskKinds = {
48
+ output: runExample,
49
+ proof: runProofExample,
50
+ };
51
+
52
+
53
+ function runExampleTasks(names, kind, reporter) {
54
+ return runTasksInParallel({
55
+ names,
56
+ workerUrl: new URL(import.meta.url),
57
+ workerData: { exampleWorker: true },
58
+ message: { kind },
59
+ maxWorkers: 3,
60
+ runLocally: (name) => exampleTaskKinds[kind](name),
61
+ onResult: (name, result) => reporter.testResult(name, result),
118
62
  });
119
63
  }
120
64
 
121
65
  function runExampleWorker() {
122
- parentPort.on('message', ({ id, name }) => {
123
- const startedAt = performance.now();
124
- try {
125
- runExample(name);
126
- parentPort.postMessage({ id, ms: Math.round(performance.now() - startedAt), error: null });
127
- } catch (error) {
128
- parentPort.postMessage({
129
- id,
130
- ms: Math.round(performance.now() - startedAt),
131
- error: { message: error?.message ?? String(error), stack: error?.stack ?? String(error) },
132
- });
133
- }
134
- });
66
+ serveTasks(parentPort, ({ kind, name }) => exampleTaskKinds[kind](name));
135
67
  }
136
68
 
137
69
 
@@ -9,11 +9,13 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { isMainThread, parentPort, workerData } from 'node:worker_threads';
12
13
 
13
14
  import { Program } from '../src/index.js';
14
15
  import { checkProofDocument, checkReportTerms, verdict } from '../src/check-proof.js';
15
16
  import { TestReporter, assertEqual, isMainModule, runStandalone } from './test-style.mjs';
16
17
  import { proofExamples } from './run-examples.mjs';
18
+ import { runTasksInParallel, serveTasks } from './parallel-tasks.mjs';
17
19
 
18
20
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
21
  const examplesDir = path.join(root, 'examples');
@@ -43,17 +45,24 @@ const KNOWN_GAPS = new Map([
43
45
  // checked here. The list is read from the directory rather than written out,
44
46
  // so a proof cannot be added without being checked.
45
47
 
46
- const totals = { steps: 0, verified: 0, redecided: 0, trusted: 0 };
47
-
48
- export function runProofChecking(reporter = new TestReporter()) {
48
+ export async function runProofChecking(reporter = new TestReporter()) {
49
49
  reporter.section('Proof checking');
50
- totals.steps = 0;
51
- totals.verified = 0;
52
- totals.redecided = 0;
53
- totals.trusted = 0;
54
- for (const name of [...proofExamples].sort()) {
55
- reporter.test(name, () => checkPackagedProof(name));
56
- }
50
+ // Each document is checked on its own against its own program, so the corpus
51
+ // is checked in parallel and the counts it establishes are summed as the
52
+ // results come back, in task order, rather than accumulated in a shared
53
+ // tally a worker could not reach.
54
+ const totals = { steps: 0, verified: 0, redecided: 0, trusted: 0 };
55
+ await runTasksInParallel({
56
+ names: [...proofExamples].sort(),
57
+ workerUrl: new URL(import.meta.url),
58
+ workerData: { proofCheckingWorker: true },
59
+ maxWorkers: 3,
60
+ runLocally: (name) => checkPackagedProof(name),
61
+ onResult: (name, result) => {
62
+ for (const field of Object.keys(totals)) totals[field] += result.value?.[field] ?? 0;
63
+ reporter.testResult(name, result);
64
+ },
65
+ });
57
66
  reporter.sectionTotal('proof checking');
58
67
  // What the corpus establishes, in the terms the conditions use: a verified
59
68
  // step was re-performed against its source clause, a recomputed one was run
@@ -83,19 +92,27 @@ function checkPackagedProof(name) {
83
92
  if (expected !== actual) {
84
93
  throw new Error(`check report mismatch for ${name}\nexpected:\n${expected}\nactual:\n${actual}`);
85
94
  }
86
- totals.steps += report.steps;
87
- totals.verified += report.verified;
88
- totals.redecided += report.redecided;
89
- totals.trusted += report.trusted.length;
95
+ // What this document contributes to the corpus totals, returned rather than
96
+ // added to a module-level tally so the same function is correct whether it
97
+ // runs here or in a worker.
98
+ const contribution = {
99
+ steps: report.steps,
100
+ verified: report.verified,
101
+ redecided: report.redecided,
102
+ trusted: report.trusted.length,
103
+ };
90
104
  const gap = KNOWN_GAPS.get(name);
91
105
  if (gap) {
92
106
  assertEqual(report.valid, false, `${name} now checks; remove it from KNOWN_GAPS (${gap})`);
93
- return;
107
+ return contribution;
94
108
  }
95
109
  const detail = report.failures.slice(0, 3).map((failure) => `[${failure.condition}] ${failure.conclusion} -- ${failure.detail}`).join('; ');
96
110
  assertEqual(report.valid, true, `${name}: ${verdict(report)}${detail ? ` -- ${detail}` : ''}`);
111
+ return contribution;
97
112
  }
98
113
 
99
- if (isMainModule(import.meta.url)) {
114
+ if (!isMainThread && workerData?.proofCheckingWorker) {
115
+ serveTasks(parentPort, ({ name }) => checkPackagedProof(name));
116
+ } else if (isMainModule(import.meta.url)) {
100
117
  await runStandalone(runProofChecking);
101
118
  }