eyeprolog 1.3.30 → 1.3.31

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.30",
6
+ "version": "1.3.31",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -55,6 +55,10 @@
55
55
  "test:wg17": "node test/run-wg17.mjs",
56
56
  "test:examples": "node test/run-examples.mjs",
57
57
  "test:regression": "node test/run-regression.mjs",
58
+ "test:regression:api": "node test/run-regression.mjs api",
59
+ "test:regression:core": "node test/run-regression.mjs regression",
60
+ "test:regression:docs": "node test/run-regression.mjs docs",
61
+ "test:regression:white-box": "node test/run-regression.mjs white-box",
58
62
  "test:playground": "node test/run-playground.mjs",
59
63
  "wg17:upgrade": "node tools/upgrade-wg17.mjs",
60
64
  "report:wg17": "node tools/report-wg17-syntax-coverage.mjs",
package/src/cli.js CHANGED
@@ -173,7 +173,7 @@ export async function main(argv) {
173
173
 
174
174
  async function loadEngine() {
175
175
  if (engineModule == null) {
176
- const [term, parser, program, solver, iso, library, write, quads] = await Promise.all([
176
+ const [term, parser, program, solver, iso, library, write, quads, execute] = await Promise.all([
177
177
  import('./term.js'),
178
178
  import('./parser.js'),
179
179
  import('./program.js'),
@@ -182,8 +182,9 @@ async function loadEngine() {
182
182
  import('./standard-library.js'),
183
183
  import('./write.js'),
184
184
  import('./quads.js'),
185
+ import('./execute.js'),
185
186
  ]);
186
- engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads };
187
+ engineModule = { ...term, ...parser, ...program, ...solver, ...iso, ...library, ...write, ...quads, ...execute };
187
188
  }
188
189
  return engineModule;
189
190
  }
@@ -201,49 +202,16 @@ async function runDefault(engine, program, options) {
201
202
  ioOptions: { write: (text) => process.stdout.write(String(text)) },
202
203
  });
203
204
  program = solver.program;
204
- const goals = options.goals.map((text) => {
205
- const goal = engine.parseGoalText(text, {
206
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
207
- operatorDefinitions: [...program.operators.values()],
208
- isoStrict: options.isoStrict,
209
- });
210
- if (goal.type === 'var') throw new engine.PrologError('instantiation_error');
211
- if (goal.type !== 'atom' && goal.type !== 'compound') throw new engine.PrologError('type_error(callable)', goal);
212
- return goal;
213
- });
214
- const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
215
- const writeOptions = {
216
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
217
- operators: [...program.operators.values()],
218
- quoted: true,
219
- };
220
- const facts = program.sourceFactLines(queriedKeys, writeOptions);
221
- const lines = new Set();
205
+ const goals = engine.normalizeGoals(options.goals, solver);
222
206
  const explanation = options.proof ? await loadExplanation() : null;
223
207
  try {
224
- solver.runInitializations();
225
- for (const goal of goals) {
226
- solver.solutionsSeen = 0;
227
- for (const env of solver.solve([goal], new engine.Env(), 0)) {
228
- if (!engine.termIsGround(goal, env)) continue;
229
-
230
- const currentWriteOptions = {
231
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
232
- operators: [...program.operators.values()],
233
- quoted: true,
234
- };
235
- const line = `${engine.formatTermForWrite(goal, env, currentWriteOptions)}.\n`;
236
- if (facts.has(line) || lines.has(line)) continue;
237
-
238
- lines.add(line);
239
-
208
+ const { haltCode } = engine.executeGoals(program, solver, goals, {
209
+ onAnswer: (line, resolved) => {
240
210
  process.stdout.write(line);
241
- if (options.proof) writeExplanation(explanation, program, engine.copyResolved(goal, env), registry);
242
- }
243
- }
244
- } catch (error) {
245
- if (error?.name !== 'HaltSignal') throw error;
246
- process.exitCode = error.code;
211
+ if (options.proof) writeExplanation(explanation, program, resolved, registry);
212
+ },
213
+ });
214
+ if (haltCode != null) process.exitCode = haltCode;
247
215
  } finally {
248
216
  if (options.stats) printStats(solver.stats);
249
217
  }
package/src/execute.js ADDED
@@ -0,0 +1,56 @@
1
+ // Shared goal preparation and execution for the CLI and embedding API.
2
+ import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround } from './term.js';
3
+ import { parseGoalText } from './parser.js';
4
+ import { HaltSignal, PrologError } from './iso.js';
5
+ import { formatTermForWrite } from './write.js';
6
+
7
+ export function normalizeGoals(requestedGoals, solver) {
8
+ return requestedGoals.map((requestedGoal) => {
9
+ const goal = typeof requestedGoal === 'string'
10
+ ? parseGoalText(requestedGoal, {
11
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
12
+ operatorDefinitions: [...solver.program.operators.values()],
13
+ isoStrict: solver.isoStrict,
14
+ })
15
+ : requestedGoal;
16
+ if (goal.type === VAR) throw new PrologError('instantiation_error');
17
+ if (goal.type !== ATOM && goal.type !== COMPOUND) throw new PrologError('type_error(callable)', goal);
18
+ return goal;
19
+ });
20
+ }
21
+
22
+ export function executeGoals(program, solver, goals, { onAnswer = () => {} } = {}) {
23
+ const initialWriteOptions = currentWriteOptions(program, solver);
24
+ const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
25
+ const facts = program.sourceFactLines(queriedKeys, initialWriteOptions);
26
+ const seen = new Set();
27
+ let haltCode = null;
28
+
29
+ try {
30
+ solver.runInitializations();
31
+ for (const goal of goals) {
32
+ solver.solutionsSeen = 0;
33
+ for (const env of solver.solve([goal], new Env(), 0)) {
34
+ if (!termIsGround(goal, env)) continue;
35
+ const resolved = copyResolved(goal, env);
36
+ const line = `${formatTermForWrite(resolved, new Env(), currentWriteOptions(program, solver))}.\n`;
37
+ if (facts.has(line) || seen.has(line)) continue;
38
+ seen.add(line);
39
+ onAnswer(line, resolved);
40
+ }
41
+ }
42
+ } catch (error) {
43
+ if (!(error instanceof HaltSignal)) throw error;
44
+ haltCode = error.code;
45
+ }
46
+
47
+ return { haltCode };
48
+ }
49
+
50
+ function currentWriteOptions(program, solver) {
51
+ return {
52
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
53
+ operators: [...program.operators.values()],
54
+ quoted: true,
55
+ };
56
+ }
package/src/index.js CHANGED
@@ -27,14 +27,12 @@ export {
27
27
  export { StreamManager } from './io.js';
28
28
  export { runQuads } from './quads.js';
29
29
 
30
- import { ATOM, COMPOUND, VAR, Env, copyResolved, termIsGround } from './term.js';
31
30
  import { Program, autoloadProgramGoals } from './program.js';
32
31
  import { Solver } from './solver.js';
33
32
  import { whyNoProof, whyProof } from './explain.js';
34
- import { HaltSignal, PrologError, getStrictIsoRegistry } from './iso.js';
33
+ import { getStrictIsoRegistry } from './iso.js';
35
34
  import { getEyePrologRegistry } from './standard-library.js';
36
- import { parseGoalText } from './parser.js';
37
- import { formatTermForWrite } from './write.js';
35
+ import { executeGoals, normalizeGoals } from './execute.js';
38
36
 
39
37
  export function run(source, options = {}) {
40
38
  const includeWhy = options.proof === true || options.why === true || options.explain === true;
@@ -67,56 +65,14 @@ export function run(source, options = {}) {
67
65
  },
68
66
  });
69
67
  program = solver.program;
70
- const goals = normalizeGoals(options, solver);
71
- const writeOptions = {
72
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
73
- operators: [...program.operators.values()],
74
- quoted: true,
75
- };
76
- const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
77
- const facts = program.sourceFactLines(queriedKeys, writeOptions);
78
- const seen = new Set();
79
- let haltCode = null;
80
- try {
81
- solver.runInitializations();
82
- for (const goal of goals) {
83
- solver.solutionsSeen = 0;
84
- for (const env of solver.solve([goal], new Env(), 0)) {
85
- const resolved = copyResolved(goal, env);
86
- if (!termIsGround(resolved)) continue;
87
- const currentWriteOptions = {
88
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
89
- operators: [...program.operators.values()],
90
- quoted: true,
91
- };
92
- const line = `${formatTermForWrite(resolved, new Env(), currentWriteOptions)}.\n`;
93
- if (facts.has(line) || seen.has(line)) continue;
94
- seen.add(line);
95
- output.push(line);
96
- if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
97
- }
98
- }
99
- } catch (error) {
100
- if (!(error instanceof HaltSignal)) throw error;
101
- haltCode = error.code;
102
- }
103
- return { stdout: output.join(''), stats: solver.stats, haltCode };
104
- }
105
-
106
- function normalizeGoals(options, solver) {
107
- const requested = options.goals ?? (options.goal == null ? [] : [options.goal]);
108
- return requested.map((requestedGoal) => {
109
- const goal = typeof requestedGoal === 'string'
110
- ? parseGoalText(requestedGoal, {
111
- doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
112
- operatorDefinitions: [...solver.program.operators.values()],
113
- isoStrict: solver.isoStrict,
114
- })
115
- : requestedGoal;
116
- if (goal.type === VAR) throw new PrologError('instantiation_error');
117
- if (goal.type !== ATOM && goal.type !== COMPOUND) throw new PrologError('type_error(callable)', goal);
118
- return goal;
68
+ const goals = normalizeGoals(requestedGoals, solver);
69
+ const { haltCode } = executeGoals(program, solver, goals, {
70
+ onAnswer: (line, resolved) => {
71
+ output.push(line);
72
+ if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
73
+ },
119
74
  });
75
+ return { stdout: output.join(''), stats: solver.stats, haltCode };
120
76
  }
121
77
 
122
78
  function appendExplanation(output, program, resolved, registry) {
package/test/run-all.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // Unified test runner used by `npm test`.
3
3
  // Running all suites in one process keeps the numbering continuous and avoids
4
4
  // npm's intermediate script banners between conformance, regression, and examples.
5
- import { TestReporter } from './test-style.mjs';
5
+ import { runStandalone } from './test-style.mjs';
6
6
  import { runConformance } from './run-conformance.mjs';
7
7
  import { runRegression } from './run-regression.mjs';
8
8
  import { runIsoStrict } from './run-iso-strict.mjs';
@@ -12,9 +12,7 @@ import { runBookExamples } from './run-book-examples.mjs';
12
12
  import { runWg17 } from './run-wg17.mjs';
13
13
  import { runOpenRuleBenchChecks } from './run-openrulebench.mjs';
14
14
 
15
- const reporter = new TestReporter();
16
-
17
- try {
15
+ await runStandalone(async (reporter) => {
18
16
  runConformance(reporter);
19
17
  runIsoStrict(reporter);
20
18
  runWg17(reporter);
@@ -23,8 +21,4 @@ try {
23
21
  await runPlayground(reporter);
24
22
  runExamples(reporter);
25
23
  runBookExamples(reporter);
26
- reporter.totalLine();
27
- process.exit(0);
28
- } catch (_) {
29
- process.exit(1);
30
- }
24
+ });
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
6
  import { Program, run } from '../src/index.js';
7
- import { TestReporter, isMainModule } from './test-style.mjs';
7
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
8
8
  import { goalsFromSource } from './goal-metadata.mjs';
9
9
 
10
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -39,11 +39,5 @@ function listPrograms(directory) {
39
39
  }
40
40
 
41
41
  if (isMainModule(import.meta.url)) {
42
- const reporter = new TestReporter();
43
- try {
44
- runBookExamples(reporter);
45
- reporter.totalLine();
46
- } catch (_) {
47
- process.exit(1);
48
- }
42
+ await runStandalone(runBookExamples);
49
43
  }
@@ -5,6 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { listPrologFiles } from './test-support.mjs';
8
9
 
9
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
10
11
  const packageRoot = path.resolve(root, '..');
@@ -24,7 +25,7 @@ export function buildConformanceReport() {
24
25
  for (const { kind, expectedKind, expectedExt, column } of KINDS) {
25
26
  const base = path.join(conformanceRoot, kind);
26
27
  if (!fs.existsSync(base)) continue;
27
- for (const file of listEyePrologFiles(base)) {
28
+ for (const file of listPrologFiles(base)) {
28
29
  const category = categoryOf(file);
29
30
  const counts = ensureCategory(categories, category);
30
31
  counts[column]++;
@@ -77,19 +78,6 @@ export function formatConformanceReport(report = buildConformanceReport()) {
77
78
  return `${lines.join('\n')}\n`;
78
79
  }
79
80
 
80
- function listEyePrologFiles(base, dir = base) {
81
- const files = [];
82
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
83
- const full = path.join(dir, entry.name);
84
- if (entry.isDirectory()) {
85
- files.push(...listEyePrologFiles(base, full));
86
- } else if (entry.isFile() && entry.name.endsWith('.pl')) {
87
- files.push(path.relative(base, full).split(path.sep).join('/'));
88
- }
89
- }
90
- return files.sort();
91
- }
92
-
93
81
  function categoryOf(file) {
94
82
  const parts = file.split('/');
95
83
  return parts.length > 1 ? parts[0] : 'legacy-numbered';
@@ -6,27 +6,12 @@ import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { Program, createDefaultRegistry, run } from '../src/index.js';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { TestReporter, isMainModule } from './test-style.mjs';
9
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
10
10
  import { goalsFromSource } from './goal-metadata.mjs';
11
+ import { listPrologFiles, withStandardModules } from './test-support.mjs';
11
12
 
12
13
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
13
14
  const filterArg = process.argv[2] ?? null;
14
- const libraryCall = /\b(?:uuid|difference|maplist|lt|gt|le|ge|between|smallest_divisor_from|random|matches|split|replace|lowercase|uppercase|trim|number_string|atom_string|term_string|append|string_concat|contains|join|substring|member|select|last|nth0|nth1|set_nth0|take|drop|slice|reverse|length|sum_list|min_list|max_list|list_to_set|countall|sumall|aggregate_min|aggregate_max)\s*\(/;
15
-
16
- function withStandardModules(text) {
17
- if (!libraryCall.test(text) || text.includes('use_module(library(')) return text;
18
- return `:- use_module(library(aggregate)).
19
- :- use_module(library(comparison)).
20
- :- use_module(library(dates)).
21
- :- use_module(library(iso_ext)).
22
- :- use_module(library(lists)).
23
- :- use_module(library(primes)).
24
- :- use_module(library(prologue), [between/3]).
25
- :- use_module(library(random)).
26
- :- use_module(library(strings)).
27
- :- use_module(library(uuid)).
28
- ${text}`;
29
- }
30
15
 
31
16
  export function runConformance(reporter = new TestReporter(), requestedFilter = null) {
32
17
  const filter = requestedFilter ?? filterArg;
@@ -42,7 +27,7 @@ export function runConformance(reporter = new TestReporter(), requestedFilter =
42
27
  function listCaseFiles(kind, filter = null) {
43
28
  const base = path.join(root, 'conformance', kind);
44
29
  if (!fs.existsSync(base)) return [];
45
- return listEyePrologFiles(base)
30
+ return listPrologFiles(base)
46
31
  .filter((name) => matchesFilter(kind, name, filter))
47
32
  .sort();
48
33
  }
@@ -58,19 +43,6 @@ function matchesFilter(kind, name, filter) {
58
43
  || `${label}/${stem}`.includes(filter);
59
44
  }
60
45
 
61
- function listEyePrologFiles(base, dir = base) {
62
- const files = [];
63
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
64
- const full = path.join(dir, entry.name);
65
- if (entry.isDirectory()) {
66
- files.push(...listEyePrologFiles(base, full));
67
- } else if (entry.isFile() && entry.name.endsWith('.pl')) {
68
- files.push(path.relative(base, full).split(path.sep).join('/'));
69
- }
70
- }
71
- return files;
72
- }
73
-
74
46
  function runCaseFile(reporter, file) {
75
47
  const name = file.slice(0, -3);
76
48
  reporter.test(name, () => runCase(name, file));
@@ -194,11 +166,5 @@ function diffText(expected, actualText) {
194
166
  }
195
167
 
196
168
  if (isMainModule(import.meta.url)) {
197
- const reporter = new TestReporter();
198
- try {
199
- runConformance(reporter);
200
- reporter.totalLine();
201
- } catch (_) {
202
- process.exit(1);
203
- }
169
+ await runStandalone(runConformance);
204
170
  }
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { Program, run } from '../src/index.js';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { TestReporter, isMainModule } from './test-style.mjs';
9
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
10
10
  import { goalsInProgramOrder } from './goal-metadata.mjs';
11
11
 
12
12
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
@@ -156,11 +156,5 @@ function diffText(expected, actualText) {
156
156
  }
157
157
 
158
158
  if (isMainModule(import.meta.url)) {
159
- const reporter = new TestReporter();
160
- try {
161
- runExamples(reporter);
162
- reporter.totalLine();
163
- } catch (_) {
164
- process.exit(1);
165
- }
159
+ await runStandalone(runExamples);
166
160
  }
@@ -10,7 +10,7 @@ import {
10
10
  parseGoalText,
11
11
  run,
12
12
  } from '../src/index.js';
13
- import { TestReporter, isMainModule } from './test-style.mjs';
13
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
14
14
 
15
15
  export function runIsoStrict(reporter = new TestReporter()) {
16
16
  reporter.section('Strict ISO core');
@@ -103,11 +103,5 @@ function includes(actual, expected, label) {
103
103
  }
104
104
 
105
105
  if (isMainModule(import.meta.url)) {
106
- const reporter = new TestReporter();
107
- try {
108
- runIsoStrict(reporter);
109
- reporter.totalLine();
110
- } catch (_) {
111
- process.exit(1);
112
- }
106
+ await runStandalone(runIsoStrict);
113
107
  }
@@ -2,57 +2,27 @@
2
2
  // Fast structural checks for the generated multi-engine OpenRuleBench corpus.
3
3
  // Full benchmark execution remains separate because it requires external
4
4
  // Prolog implementations and is intentionally performance-oriented.
5
- import path from 'node:path';
6
- import process from 'node:process';
7
- import { spawnSync } from 'node:child_process';
8
- import { fileURLToPath } from 'node:url';
9
- import { TestReporter, isMainModule } from './test-style.mjs';
10
-
11
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
5
+ import { validateOpenRuleBench } from '../openrulebench/tools/check.mjs';
6
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
12
7
 
13
8
  export function runOpenRuleBenchChecks(reporter = new TestReporter()) {
9
+ let report = null;
14
10
  reporter.section('OpenRuleBench source integrity');
15
- runChecker(
16
- reporter,
17
- 'generated sources pass lexical checks',
18
- 'openrulebench/tools/check_sources.mjs',
19
- 'eyeprolog: 14 sources; lexical checks ok',
20
- );
21
- runChecker(
22
- reporter,
23
- 'engine variants preserve table and WFS adaptations',
24
- 'openrulebench/tools/check_multiengine.mjs',
25
- 'OK: 14 benchmarks x 4 engines; table/WFS adaptations verified.',
26
- );
11
+ reporter.test('generated sources pass lexical checks', () => {
12
+ report = validateOpenRuleBench();
13
+ assertNoErrors(report.lexicalErrors);
14
+ });
15
+ reporter.test('engine variants preserve table and WFS adaptations', () => {
16
+ report ??= validateOpenRuleBench();
17
+ assertNoErrors(report.adaptationErrors);
18
+ });
27
19
  reporter.sectionTotal('OpenRuleBench source-integrity');
28
20
  }
29
21
 
30
- function runChecker(reporter, name, relativeScript, expectedOutput) {
31
- reporter.test(name, () => {
32
- const result = spawnSync(process.execPath, [relativeScript], {
33
- cwd: packageRoot,
34
- encoding: 'utf8',
35
- timeout: 30000,
36
- });
37
- if (result.error) throw result.error;
38
- if (result.status !== 0) {
39
- throw new Error(
40
- `${relativeScript} exited with ${result.status}\n` +
41
- `${result.stdout ?? ''}${result.stderr ?? ''}`.trimEnd(),
42
- );
43
- }
44
- if (!String(result.stdout).includes(expectedOutput)) {
45
- throw new Error(`${relativeScript} did not report its expected summary\n${result.stdout ?? ''}`.trimEnd());
46
- }
47
- });
22
+ function assertNoErrors(errors) {
23
+ if (errors.length > 0) throw new Error(errors.join('\n'));
48
24
  }
49
25
 
50
26
  if (isMainModule(import.meta.url)) {
51
- const reporter = new TestReporter();
52
- try {
53
- runOpenRuleBenchChecks(reporter);
54
- reporter.totalLine();
55
- } catch (_) {
56
- process.exit(1);
57
- }
27
+ await runStandalone(runOpenRuleBenchChecks);
58
28
  }
@@ -10,7 +10,14 @@ import {
10
10
  executePlaygroundRequest,
11
11
  installPlaygroundWorker,
12
12
  } from '../src/playground-worker.js';
13
- import { TestReporter, isMainModule } from './test-style.mjs';
13
+ import {
14
+ TestReporter,
15
+ assertEqual,
16
+ assertIncludes,
17
+ assertNotIncludes,
18
+ isMainModule,
19
+ runStandalone,
20
+ } from './test-style.mjs';
14
21
 
15
22
  const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
16
23
  const packageRoot = path.resolve(testRoot, '..');
@@ -220,30 +227,6 @@ function assert(condition, message) {
220
227
  if (!condition) throw new Error(message);
221
228
  }
222
229
 
223
- function assertEqual(actual, expected, label) {
224
- if (actual !== expected) {
225
- throw new Error(`${label} mismatch\nexpected: ${JSON.stringify(expected)}\nactual: ${JSON.stringify(actual)}`);
226
- }
227
- }
228
-
229
- function assertIncludes(actual, expected, label) {
230
- if (!String(actual).includes(expected)) {
231
- throw new Error(`${label} did not include ${JSON.stringify(expected)}\nactual: ${JSON.stringify(actual)}`);
232
- }
233
- }
234
-
235
- function assertNotIncludes(actual, expected, label) {
236
- if (String(actual).includes(expected)) {
237
- throw new Error(`${label} unexpectedly included ${JSON.stringify(expected)}`);
238
- }
239
- }
240
-
241
230
  if (isMainModule(import.meta.url)) {
242
- const reporter = new TestReporter();
243
- try {
244
- await runPlayground(reporter);
245
- reporter.totalLine();
246
- } catch (_) {
247
- process.exit(1);
248
- }
231
+ await runStandalone(runPlayground);
249
232
  }
@@ -48,13 +48,21 @@ import { PrologError, formalErrorTerm } from '../src/iso.js';
48
48
  import { compareTerms } from '../src/term.js';
49
49
  import { formatTermForWrite } from '../src/write.js';
50
50
  import { selectClauseCandidates } from '../src/program.js';
51
- import { TestReporter, isMainModule } from './test-style.mjs';
51
+ import {
52
+ TestReporter,
53
+ assertEqual,
54
+ assertIncludes,
55
+ assertNotIncludes,
56
+ isMainModule,
57
+ runStandalone,
58
+ } from './test-style.mjs';
52
59
  import { buildConformanceReport, formatConformanceReport } from './run-conformance-report.mjs';
53
60
  import { proofExamples } from './run-examples.mjs';
54
61
  import { goalsFromSource } from './goal-metadata.mjs';
55
62
  import { renderWg17SyntaxStatus } from '../tools/report-wg17-syntax-coverage.mjs';
56
63
  import { parseWg17SyntaxTable } from '../tools/upgrade-wg17.mjs';
57
64
  import { executeWg17Item, matchesUpstreamExpectation, readWg17SyntaxFixture } from './run-wg17.mjs';
65
+ import { withStandardModules } from './test-support.mjs';
58
66
 
59
67
  const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
60
68
  const packageRoot = path.resolve(testRoot, '..');
@@ -62,22 +70,6 @@ const bin = path.join(packageRoot, 'bin', 'eyeprolog.js');
62
70
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
63
71
  let tmp = null;
64
72
  let tmpCounter = 0;
65
- const libraryCall = /\b(?:uuid|difference|maplist|foldl|call_nth|lt|gt|le|ge|between|smallest_divisor_from|random|matches|split|replace|lowercase|uppercase|trim|number_string|atom_string|term_string|append|string_concat|contains|join|substring|member|select|last|nth0|nth1|set_nth0|take|drop|slice|reverse|length|sum_list|min_list|max_list|list_to_set|countall|sumall|aggregate_min|aggregate_max)\s*\(/;
66
-
67
- function withStandardModules(source) {
68
- if (!libraryCall.test(source) || source.includes('use_module(library(') || source.includes(':- module(')) return source;
69
- return `:- use_module(library(aggregate)).
70
- :- use_module(library(comparison)).
71
- :- use_module(library(dates)).
72
- :- use_module(library(iso_ext)).
73
- :- use_module(library(lists)).
74
- :- use_module(library(primes)).
75
- :- use_module(library(prologue), [between/3]).
76
- :- use_module(library(random)).
77
- :- use_module(library(strings)).
78
- :- use_module(library(uuid)).
79
- ${source}`;
80
- }
81
73
 
82
74
  function run(source, options = {}) {
83
75
  const programSource = Array.isArray(source) ? source.join('\n') : source;
@@ -92,15 +84,24 @@ function sourceAtom(value) {
92
84
  return `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "''")}'`;
93
85
  }
94
86
 
95
- export function runRegression(reporter = new TestReporter()) {
87
+ export function runRegression(reporter = new TestReporter(), requestedSection = null) {
88
+ const sections = [
89
+ { key: 'regression', name: 'Regression', cases: regressionCases },
90
+ { key: 'docs', name: 'Documentation sync', cases: documentationSyncCases },
91
+ { key: 'api', name: 'API', cases: apiCases },
92
+ { key: 'white-box', name: 'White-box', cases: whiteBoxCases },
93
+ ];
94
+ const selected = requestedSection == null
95
+ ? sections
96
+ : sections.filter((section) => section.key === requestedSection);
97
+ if (selected.length === 0) {
98
+ throw new Error(`unknown regression section: ${requestedSection}; expected ${sections.map(({ key }) => key).join(', ')}`);
99
+ }
100
+
96
101
  tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-regression.'));
97
102
  tmpCounter = 0;
98
-
99
103
  try {
100
- runSection(reporter, 'Regression', regressionCases());
101
- runSection(reporter, 'Documentation sync', documentationSyncCases());
102
- runSection(reporter, 'API', apiCases());
103
- runSection(reporter, 'White-box', whiteBoxCases());
104
+ for (const section of selected) runSection(reporter, section.name, section.cases());
104
105
  } finally {
105
106
  fs.rmSync(tmp, { recursive: true, force: true });
106
107
  tmp = null;
@@ -5739,18 +5740,6 @@ function runCli(args, options = {}) {
5739
5740
  });
5740
5741
  }
5741
5742
 
5742
- function assertEqual(actual, expected, label) {
5743
- if (actual !== expected) throw new Error(`${label} mismatch\nexpected: ${format(expected)}\nactual: ${format(actual)}`);
5744
- }
5745
-
5746
- function assertIncludes(actual, expected, label) {
5747
- if (!actual.includes(expected)) throw new Error(`${label} did not include ${format(expected)}\nactual: ${format(actual)}`);
5748
- }
5749
-
5750
- function assertNotIncludes(actual, expected, label) {
5751
- if (String(actual).includes(expected)) throw new Error(`${label} unexpectedly included ${format(expected)}\nactual: ${format(actual)}`);
5752
- }
5753
-
5754
5743
  function arrayDiffMessages(actual, expected, label) {
5755
5744
  const messages = [];
5756
5745
  const actualSet = new Set(actual);
@@ -5776,11 +5765,5 @@ function format(value) {
5776
5765
  }
5777
5766
 
5778
5767
  if (isMainModule(import.meta.url)) {
5779
- const reporter = new TestReporter();
5780
- try {
5781
- runRegression(reporter);
5782
- reporter.totalLine();
5783
- } catch (_) {
5784
- process.exit(1);
5785
- }
5768
+ await runStandalone((reporter) => runRegression(reporter, process.argv[2] ?? null));
5786
5769
  }
package/test/run-wg17.mjs CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  } from '../src/index.js';
12
12
  import { parseTermText } from '../src/parser.js';
13
13
  import { variantTerms } from '../src/term.js';
14
- import { TestReporter, isMainModule } from './test-style.mjs';
14
+ import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
15
15
 
16
16
  const testRoot = path.dirname(fileURLToPath(import.meta.url));
17
17
  const fixturePath = path.join(testRoot, 'conformance', 'wg17-syntax-cases.json');
@@ -377,12 +377,5 @@ export function runWg17(reporter = new TestReporter()) {
377
377
  }
378
378
 
379
379
  if (isMainModule(import.meta.url)) {
380
- const reporter = new TestReporter();
381
- try {
382
- runWg17(reporter);
383
- reporter.totalLine();
384
- } catch (error) {
385
- process.stderr.write(`${error?.stack ?? error}\n`);
386
- process.exitCode = 1;
387
- }
380
+ await runStandalone(runWg17);
388
381
  }
@@ -102,6 +102,43 @@ export function isMainModule(metaUrl) {
102
102
  return process.argv[1] != null && path.resolve(process.argv[1]) === fileURLToPath(metaUrl);
103
103
  }
104
104
 
105
+ export async function runStandalone(runSuite) {
106
+ const reporter = new TestReporter();
107
+ try {
108
+ await runSuite(reporter);
109
+ reporter.totalLine();
110
+ } catch (error) {
111
+ // TestReporter already prints failures raised inside a test. Preserve a
112
+ // diagnostic for setup/teardown failures that occur outside reporter.test.
113
+ if (reporter.ok === reporter.total) {
114
+ reporter.stderr.write(`${error?.stack ?? String(error)}\n`);
115
+ }
116
+ process.exitCode = 1;
117
+ }
118
+ }
119
+
120
+ export function assertEqual(actual, expected, label) {
121
+ if (actual !== expected) {
122
+ throw new Error(`${label} mismatch\nexpected: ${formatValue(expected)}\nactual: ${formatValue(actual)}`);
123
+ }
124
+ }
125
+
126
+ export function assertIncludes(actual, expected, label) {
127
+ if (!String(actual).includes(expected)) {
128
+ throw new Error(`${label} did not include ${formatValue(expected)}\nactual: ${formatValue(actual)}`);
129
+ }
130
+ }
131
+
132
+ export function assertNotIncludes(actual, expected, label) {
133
+ if (String(actual).includes(expected)) {
134
+ throw new Error(`${label} unexpectedly included ${formatValue(expected)}\nactual: ${formatValue(actual)}`);
135
+ }
136
+ }
137
+
138
+ function formatValue(value) {
139
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
140
+ }
141
+
105
142
  function defaultSectionLabel(name) {
106
143
  return String(name)
107
144
  .replace(/^Conformance\s+/, 'conformance ')
@@ -0,0 +1,36 @@
1
+ // Shared fixtures and filesystem helpers used across test suites.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const libraryCall = /\b(?:uuid|difference|maplist|foldl|call_nth|lt|gt|le|ge|between|smallest_divisor_from|random|matches|split|replace|lowercase|uppercase|trim|number_string|atom_string|term_string|append|string_concat|contains|join|substring|member|select|last|nth0|nth1|set_nth0|take|drop|slice|reverse|length|sum_list|min_list|max_list|list_to_set|countall|sumall|aggregate_min|aggregate_max)\s*\(/;
6
+
7
+ const standardModulePrelude = `:- use_module(library(aggregate)).
8
+ :- use_module(library(comparison)).
9
+ :- use_module(library(dates)).
10
+ :- use_module(library(iso_ext)).
11
+ :- use_module(library(lists)).
12
+ :- use_module(library(primes)).
13
+ :- use_module(library(prologue), [between/3]).
14
+ :- use_module(library(random)).
15
+ :- use_module(library(strings)).
16
+ :- use_module(library(uuid)).
17
+ `;
18
+
19
+ export function withStandardModules(source) {
20
+ const text = String(source);
21
+ if (!libraryCall.test(text) || text.includes('use_module(library(') || text.includes(':- module(')) return text;
22
+ return `${standardModulePrelude}${text}`;
23
+ }
24
+
25
+ export function listPrologFiles(base, dir = base) {
26
+ const files = [];
27
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
28
+ const full = path.join(dir, entry.name);
29
+ if (entry.isDirectory()) {
30
+ files.push(...listPrologFiles(base, full));
31
+ } else if (entry.isFile() && entry.name.endsWith('.pl')) {
32
+ files.push(path.relative(base, full).split(path.sep).join('/'));
33
+ }
34
+ }
35
+ return files.sort();
36
+ }