eyeprolog 1.3.62 → 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.62",
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
  }
@@ -2221,22 +2221,16 @@ c4 ?- call((!;1)).
2221
2221
  {
2222
2222
  name: 'REPL term input is on demand in conjunctions and Ctrl-D does not exit the top level',
2223
2223
  run: () => {
2224
- if (process.platform === 'win32') return;
2225
- const available = spawnSync('sh', ['-c',
2226
- 'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
2227
- if (available.status !== 0) return;
2228
- const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
2229
- const scriptCommand =
2230
- `{ printf 'read(X), read(Y).\n'; sleep 0.3; ` +
2231
- `printf 'foo.\n'; sleep 0.3; printf 'bar.\n'; sleep 0.3; ` +
2232
- `printf 'read(Z).\n'; sleep 0.3; printf '\\004'; sleep 0.3; ` +
2233
- `printf 'true.\n'; sleep 0.3; printf 'halt.\n'; } | ` +
2234
- `script -qefc ${shellQuote(command)} /dev/null`;
2235
- const result = spawnSync('sh', ['-c', scriptCommand], {
2236
- cwd: packageRoot,
2237
- encoding: 'utf8',
2238
- timeout: 5000,
2239
- });
2224
+ if (!hasUtilLinuxScript()) return;
2225
+ const result = runScriptedRepl([
2226
+ { waitFor: '?- ', send: 'read(X), read(Y).\n' },
2227
+ { waitFor: ' |: ', send: 'foo.\n' },
2228
+ { waitFor: '|: ', send: 'bar.\n' },
2229
+ { waitFor: 'X = foo, Y = bar.', send: 'read(Z).\n' },
2230
+ { waitFor: ' |: ', send: '\u0004' },
2231
+ { waitFor: 'Z = end_of_file.', send: 'true.\n' },
2232
+ { waitFor: ' true.', send: 'halt.\n' },
2233
+ ]);
2240
2234
  assertEqual(result.error?.code, undefined, 'interactive read timeout');
2241
2235
  assertEqual(result.status, 0, 'exit status');
2242
2236
  assertIncludes(result.stdout, 'X = foo, Y = bar.', 'conjunction reads');
@@ -2248,23 +2242,18 @@ c4 ?- call((!;1)).
2248
2242
  {
2249
2243
  name: 'REPL character input is on demand and Ctrl-D stays local (issue #55)',
2250
2244
  run: () => {
2251
- if (process.platform === 'win32') return;
2252
- const available = spawnSync('sh', ['-c',
2253
- 'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
2254
- if (available.status !== 0) return;
2255
- const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
2256
- const scriptCommand =
2257
- `{ printf 'peek_char(P), get_char(C), get_code(K).\n'; sleep 0.3; ` +
2258
- `printf 'ab\n'; sleep 0.3; printf 'get_char(N).\n'; sleep 0.3; ` +
2259
- `printf 'z\n'; sleep 0.3; printf 'get_char(E).\n'; sleep 0.3; ` +
2260
- `printf '\\004'; sleep 0.3; printf 'get_char(D).\n'; sleep 0.3; ` +
2261
- `printf 'q\n'; sleep 0.3; printf 'halt.\n'; } | ` +
2262
- `script -qefc ${shellQuote(command)} /dev/null`;
2263
- const result = spawnSync('sh', ['-c', scriptCommand], {
2264
- cwd: packageRoot,
2265
- encoding: 'utf8',
2266
- timeout: 5000,
2267
- });
2245
+ if (!hasUtilLinuxScript()) return;
2246
+ const result = runScriptedRepl([
2247
+ { waitFor: '?- ', send: 'peek_char(P), get_char(C), get_code(K).\n' },
2248
+ { waitFor: ' |: ', send: 'ab\n' },
2249
+ { waitFor: 'P = a, C = a, K = 98.', send: 'get_char(N).\n' },
2250
+ { waitFor: ' |: ', send: 'z\n' },
2251
+ { waitFor: 'N = z.', send: 'get_char(E).\n' },
2252
+ { waitFor: ' |: ', send: '\u0004' },
2253
+ { waitFor: 'E = end_of_file.', send: 'get_char(D).\n' },
2254
+ { waitFor: ' |: ', send: 'q\n' },
2255
+ { waitFor: 'D = q.', send: 'halt.\n' },
2256
+ ]);
2268
2257
  assertEqual(result.error?.code, undefined, 'interactive character input timeout');
2269
2258
  assertEqual(result.status, 0, 'exit status');
2270
2259
  assertIncludes(result.stdout, 'P = a, C = a, K = 98.', 'peek/get char and code input');
@@ -6112,6 +6101,99 @@ function shellQuote(value) {
6112
6101
  return `'${String(value).replaceAll("'", "'\"'\"'")}'`;
6113
6102
  }
6114
6103
 
6104
+ let utilLinuxScriptAvailable = null;
6105
+
6106
+ function hasUtilLinuxScript() {
6107
+ if (process.platform === 'win32') return false;
6108
+ if (utilLinuxScriptAvailable == null) {
6109
+ const available = spawnSync('sh', ['-c',
6110
+ 'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
6111
+ utilLinuxScriptAvailable = available.status === 0;
6112
+ }
6113
+ return utilLinuxScriptAvailable;
6114
+ }
6115
+
6116
+ function runScriptedRepl(steps, { timeout = 5000 } = {}) {
6117
+ const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
6118
+ const payload = Buffer.from(JSON.stringify({
6119
+ command,
6120
+ cwd: packageRoot,
6121
+ steps,
6122
+ timeout,
6123
+ })).toString('base64');
6124
+ const helper = String.raw`
6125
+ const { spawn } = require('node:child_process');
6126
+ const config = JSON.parse(Buffer.from(process.env.EYEPROLOG_REPL_SCRIPT, 'base64').toString('utf8'));
6127
+ const child = spawn('script', ['-qefc', config.command, '/dev/null'], {
6128
+ cwd: config.cwd,
6129
+ stdio: ['pipe', 'pipe', 'pipe'],
6130
+ });
6131
+ let stdout = '';
6132
+ let stderr = '';
6133
+ let cursor = 0;
6134
+ let step = 0;
6135
+ let finished = false;
6136
+
6137
+ function advance() {
6138
+ while (step < config.steps.length) {
6139
+ const current = config.steps[step];
6140
+ const index = stdout.indexOf(current.waitFor, cursor);
6141
+ if (index === -1) return;
6142
+ cursor = index + current.waitFor.length;
6143
+ child.stdin.write(current.send);
6144
+ step++;
6145
+ }
6146
+ }
6147
+
6148
+ child.stdout.on('data', (chunk) => {
6149
+ stdout += chunk;
6150
+ advance();
6151
+ });
6152
+ child.stderr.on('data', (chunk) => {
6153
+ stderr += chunk;
6154
+ });
6155
+
6156
+ const timer = setTimeout(() => {
6157
+ if (finished) return;
6158
+ finished = true;
6159
+ child.kill('SIGKILL');
6160
+ process.stdout.write(JSON.stringify({ status: null, stdout, stderr, timedOut: true, step }));
6161
+ }, config.timeout);
6162
+
6163
+ child.on('close', (status, signal) => {
6164
+ if (finished) return;
6165
+ finished = true;
6166
+ clearTimeout(timer);
6167
+ process.stdout.write(JSON.stringify({ status, signal, stdout, stderr, timedOut: false, step }));
6168
+ });
6169
+ `;
6170
+ const helperResult = spawnSync(process.execPath, ['-e', helper], {
6171
+ cwd: packageRoot,
6172
+ encoding: 'utf8',
6173
+ env: { ...process.env, EYEPROLOG_REPL_SCRIPT: payload },
6174
+ timeout: timeout + 1000,
6175
+ });
6176
+ if (helperResult.error) return helperResult;
6177
+ if (helperResult.status !== 0) return helperResult;
6178
+ let result;
6179
+ try {
6180
+ result = JSON.parse(helperResult.stdout);
6181
+ } catch (error) {
6182
+ return {
6183
+ ...helperResult,
6184
+ error: new Error(`interactive REPL helper returned invalid JSON: ${error.message}`),
6185
+ };
6186
+ }
6187
+ return {
6188
+ status: result.status,
6189
+ stdout: result.stdout,
6190
+ stderr: `${result.stderr ?? ''}${helperResult.stderr ?? ''}`,
6191
+ error: result.timedOut
6192
+ ? Object.assign(new Error(`interactive REPL timed out after step ${result.step}`), { code: 'ETIMEDOUT' })
6193
+ : undefined,
6194
+ };
6195
+ }
6196
+
6115
6197
  function runCli(args, options = {}) {
6116
6198
  return spawnSync(process.execPath, [bin, ...args], {
6117
6199
  cwd: packageRoot,
@@ -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`);