eyeprolog 1.3.29 → 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/README.md +9 -2
- package/package.json +6 -1
- package/src/cli.js +10 -42
- package/src/execute.js +56 -0
- package/src/index.js +11 -54
- package/src/program.js +42 -0
- package/test/run-all.mjs +5 -9
- package/test/run-book-examples.mjs +2 -8
- package/test/run-conformance-report.mjs +2 -14
- package/test/run-conformance.mjs +4 -38
- package/test/run-examples.mjs +2 -8
- package/test/run-iso-strict.mjs +2 -8
- package/test/run-openrulebench.mjs +28 -0
- package/test/run-playground.mjs +9 -26
- package/test/run-regression.mjs +57 -42
- package/test/run-wg17.mjs +2 -9
- package/test/test-style.mjs +37 -0
- package/test/test-support.mjs +36 -0
- package/the-art-of-eyeprolog.md +9 -2
package/README.md
CHANGED
|
@@ -291,8 +291,11 @@ and Scryer (`scryer-prolog`) installed, run:
|
|
|
291
291
|
npm run test:interop
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
This optional check runs the same portable Towers of Hanoi source under
|
|
295
|
+
EyeProlog, Trealla, and Scryer. The default `npm test` does not require external
|
|
296
|
+
Prolog implementations; it does validate all four generated OpenRuleBench
|
|
297
|
+
source trees and their engine-specific tabling and WFS adaptations. Run those
|
|
298
|
+
fast structural checks separately with `npm run test:openrulebench`.
|
|
296
299
|
|
|
297
300
|
## Development
|
|
298
301
|
|
|
@@ -303,4 +306,8 @@ npm install
|
|
|
303
306
|
npm test
|
|
304
307
|
```
|
|
305
308
|
|
|
309
|
+
The GitHub test workflow runs the complete suite and an npm package dry-run on
|
|
310
|
+
both the minimum supported Node.js 18 release line and Node.js 24. Publishing
|
|
311
|
+
repeats those release checks before uploading the package.
|
|
312
|
+
|
|
306
313
|
EyeProlog is released under the [MIT License](LICENSE.md).
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.3.
|
|
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",
|
|
@@ -51,9 +51,14 @@
|
|
|
51
51
|
"test:conformance": "node test/run-conformance.mjs",
|
|
52
52
|
"test:iso-strict": "node test/run-iso-strict.mjs",
|
|
53
53
|
"test:interop": "node test/run-interop.mjs",
|
|
54
|
+
"test:openrulebench": "node test/run-openrulebench.mjs",
|
|
54
55
|
"test:wg17": "node test/run-wg17.mjs",
|
|
55
56
|
"test:examples": "node test/run-examples.mjs",
|
|
56
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",
|
|
57
62
|
"test:playground": "node test/run-playground.mjs",
|
|
58
63
|
"wg17:upgrade": "node tools/upgrade-wg17.mjs",
|
|
59
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
|
|
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
|
-
|
|
225
|
-
|
|
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,
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
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 {
|
|
31
|
-
import { Program } from './program.js';
|
|
30
|
+
import { Program, autoloadProgramGoals } from './program.js';
|
|
32
31
|
import { Solver } from './solver.js';
|
|
33
32
|
import { whyNoProof, whyProof } from './explain.js';
|
|
34
|
-
import {
|
|
33
|
+
import { getStrictIsoRegistry } from './iso.js';
|
|
35
34
|
import { getEyePrologRegistry } from './standard-library.js';
|
|
36
|
-
import {
|
|
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;
|
|
@@ -49,6 +47,7 @@ export function run(source, options = {}) {
|
|
|
49
47
|
autoloadGoals: requestedGoals,
|
|
50
48
|
};
|
|
51
49
|
let program = source instanceof Program ? source : Program.parse(source, parseOptions);
|
|
50
|
+
if (source instanceof Program) autoloadProgramGoals(program, requestedGoals, options);
|
|
52
51
|
const strictIso = requestedStrictIso || program.strictIso === true;
|
|
53
52
|
const runOptions = strictIso
|
|
54
53
|
? { ...options, isoStrict: true, registry: getStrictIsoRegistry() }
|
|
@@ -66,56 +65,14 @@ export function run(source, options = {}) {
|
|
|
66
65
|
},
|
|
67
66
|
});
|
|
68
67
|
program = solver.program;
|
|
69
|
-
const goals = normalizeGoals(
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
|
|
76
|
-
const facts = program.sourceFactLines(queriedKeys, writeOptions);
|
|
77
|
-
const seen = new Set();
|
|
78
|
-
let haltCode = null;
|
|
79
|
-
try {
|
|
80
|
-
solver.runInitializations();
|
|
81
|
-
for (const goal of goals) {
|
|
82
|
-
solver.solutionsSeen = 0;
|
|
83
|
-
for (const env of solver.solve([goal], new Env(), 0)) {
|
|
84
|
-
const resolved = copyResolved(goal, env);
|
|
85
|
-
if (!termIsGround(resolved)) continue;
|
|
86
|
-
const currentWriteOptions = {
|
|
87
|
-
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
88
|
-
operators: [...program.operators.values()],
|
|
89
|
-
quoted: true,
|
|
90
|
-
};
|
|
91
|
-
const line = `${formatTermForWrite(resolved, new Env(), currentWriteOptions)}.\n`;
|
|
92
|
-
if (facts.has(line) || seen.has(line)) continue;
|
|
93
|
-
seen.add(line);
|
|
94
|
-
output.push(line);
|
|
95
|
-
if (includeWhy) appendExplanation(output, program, resolved, runOptions.registry);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (error) {
|
|
99
|
-
if (!(error instanceof HaltSignal)) throw error;
|
|
100
|
-
haltCode = error.code;
|
|
101
|
-
}
|
|
102
|
-
return { stdout: output.join(''), stats: solver.stats, haltCode };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function normalizeGoals(options, solver) {
|
|
106
|
-
const requested = options.goals ?? (options.goal == null ? [] : [options.goal]);
|
|
107
|
-
return requested.map((requestedGoal) => {
|
|
108
|
-
const goal = typeof requestedGoal === 'string'
|
|
109
|
-
? parseGoalText(requestedGoal, {
|
|
110
|
-
doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
|
|
111
|
-
operatorDefinitions: [...solver.program.operators.values()],
|
|
112
|
-
isoStrict: solver.isoStrict,
|
|
113
|
-
})
|
|
114
|
-
: requestedGoal;
|
|
115
|
-
if (goal.type === VAR) throw new PrologError('instantiation_error');
|
|
116
|
-
if (goal.type !== ATOM && goal.type !== COMPOUND) throw new PrologError('type_error(callable)', goal);
|
|
117
|
-
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
|
+
},
|
|
118
74
|
});
|
|
75
|
+
return { stdout: output.join(''), stats: solver.stats, haltCode };
|
|
119
76
|
}
|
|
120
77
|
|
|
121
78
|
function appendExplanation(output, program, resolved, registry) {
|
package/src/program.js
CHANGED
|
@@ -706,6 +706,48 @@ function buildProgramFromSources(sources, options) {
|
|
|
706
706
|
return builder.finish();
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
+
// Program.parse() can see host-supplied goals through autoloadGoals while it
|
|
710
|
+
// builds the program. A Program passed directly to run() has already completed
|
|
711
|
+
// that phase, so extend the existing instance with the same canonical interop
|
|
712
|
+
// imports before solving those goals. Keeping this here lets both paths share
|
|
713
|
+
// the dependency analysis and module-loading rules.
|
|
714
|
+
export function autoloadProgramGoals(program, inputs, options = {}) {
|
|
715
|
+
if (inputs == null || (Array.isArray(inputs) && inputs.length === 0)) return program;
|
|
716
|
+
const operatorState = createParserOperatorState(
|
|
717
|
+
[...program.operators.values()],
|
|
718
|
+
false,
|
|
719
|
+
{ isoStrict: program.strictIso },
|
|
720
|
+
);
|
|
721
|
+
const parserFlagState = { doubleQuotes: program.doubleQuotes ?? options.doubleQuotes ?? 'chars' };
|
|
722
|
+
const goals = parseInteropGoalInputs(inputs, {
|
|
723
|
+
...options,
|
|
724
|
+
isoStrict: program.strictIso,
|
|
725
|
+
operatorState,
|
|
726
|
+
parserFlagState,
|
|
727
|
+
}, program);
|
|
728
|
+
|
|
729
|
+
if (!program.strictIso && options.autoload !== false) {
|
|
730
|
+
const builder = new ProgramBuilder({ ...options, isoStrict: false }, program);
|
|
731
|
+
const loadedModules = new Set([...program.modules.keys()].filter((name) => name !== 'user'));
|
|
732
|
+
const autoloadCount = program.autoloadedPredicates.length;
|
|
733
|
+
autoloadInteropDependencies(builder, {
|
|
734
|
+
...options,
|
|
735
|
+
isoStrict: false,
|
|
736
|
+
operatorState,
|
|
737
|
+
parserFlagState,
|
|
738
|
+
}, new Set(), loadedModules, goals);
|
|
739
|
+
if (program.autoloadedPredicates.length !== autoloadCount) {
|
|
740
|
+
// Existing Solver instances key their tables by Program revision, and a
|
|
741
|
+
// previously requested negation analysis is no longer complete once new
|
|
742
|
+
// library groups become reachable.
|
|
743
|
+
program.noteMutation(false);
|
|
744
|
+
builder.finish();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
analyzeInteropPortability(program, goals);
|
|
748
|
+
return program;
|
|
749
|
+
}
|
|
750
|
+
|
|
709
751
|
function loadSourcesIntoBuilder(builder, sources, options, fast) {
|
|
710
752
|
const ensured = new Set();
|
|
711
753
|
const loadedModules = new Set();
|
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 {
|
|
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';
|
|
@@ -10,19 +10,15 @@ import { runPlayground } from './run-playground.mjs';
|
|
|
10
10
|
import { runExamples } from './run-examples.mjs';
|
|
11
11
|
import { runBookExamples } from './run-book-examples.mjs';
|
|
12
12
|
import { runWg17 } from './run-wg17.mjs';
|
|
13
|
+
import { runOpenRuleBenchChecks } from './run-openrulebench.mjs';
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
15
|
+
await runStandalone(async (reporter) => {
|
|
17
16
|
runConformance(reporter);
|
|
18
17
|
runIsoStrict(reporter);
|
|
19
18
|
runWg17(reporter);
|
|
19
|
+
runOpenRuleBenchChecks(reporter);
|
|
20
20
|
runRegression(reporter);
|
|
21
21
|
await runPlayground(reporter);
|
|
22
22
|
runExamples(reporter);
|
|
23
23
|
runBookExamples(reporter);
|
|
24
|
-
|
|
25
|
-
process.exit(0);
|
|
26
|
-
} catch (_) {
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
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
|
-
|
|
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
|
|
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';
|
package/test/run-conformance.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
198
|
-
try {
|
|
199
|
-
runConformance(reporter);
|
|
200
|
-
reporter.totalLine();
|
|
201
|
-
} catch (_) {
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
169
|
+
await runStandalone(runConformance);
|
|
204
170
|
}
|
package/test/run-examples.mjs
CHANGED
|
@@ -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
|
-
|
|
160
|
-
try {
|
|
161
|
-
runExamples(reporter);
|
|
162
|
-
reporter.totalLine();
|
|
163
|
-
} catch (_) {
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
159
|
+
await runStandalone(runExamples);
|
|
166
160
|
}
|
package/test/run-iso-strict.mjs
CHANGED
|
@@ -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
|
-
|
|
107
|
-
try {
|
|
108
|
-
runIsoStrict(reporter);
|
|
109
|
-
reporter.totalLine();
|
|
110
|
-
} catch (_) {
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
106
|
+
await runStandalone(runIsoStrict);
|
|
113
107
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fast structural checks for the generated multi-engine OpenRuleBench corpus.
|
|
3
|
+
// Full benchmark execution remains separate because it requires external
|
|
4
|
+
// Prolog implementations and is intentionally performance-oriented.
|
|
5
|
+
import { validateOpenRuleBench } from '../openrulebench/tools/check.mjs';
|
|
6
|
+
import { TestReporter, isMainModule, runStandalone } from './test-style.mjs';
|
|
7
|
+
|
|
8
|
+
export function runOpenRuleBenchChecks(reporter = new TestReporter()) {
|
|
9
|
+
let report = null;
|
|
10
|
+
reporter.section('OpenRuleBench source integrity');
|
|
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
|
+
});
|
|
19
|
+
reporter.sectionTotal('OpenRuleBench source-integrity');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertNoErrors(errors) {
|
|
23
|
+
if (errors.length > 0) throw new Error(errors.join('\n'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isMainModule(import.meta.url)) {
|
|
27
|
+
await runStandalone(runOpenRuleBenchChecks);
|
|
28
|
+
}
|
package/test/run-playground.mjs
CHANGED
|
@@ -10,7 +10,14 @@ import {
|
|
|
10
10
|
executePlaygroundRequest,
|
|
11
11
|
installPlaygroundWorker,
|
|
12
12
|
} from '../src/playground-worker.js';
|
|
13
|
-
import {
|
|
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
|
-
|
|
243
|
-
try {
|
|
244
|
-
await runPlayground(reporter);
|
|
245
|
-
reporter.totalLine();
|
|
246
|
-
} catch (_) {
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
231
|
+
await runStandalone(runPlayground);
|
|
249
232
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -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 {
|
|
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,
|
|
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;
|
|
@@ -3407,6 +3408,23 @@ function documentationSyncCases() {
|
|
|
3407
3408
|
name: 'documented npm scripts exist in package.json',
|
|
3408
3409
|
run: () => assertArrayEqual(missingDocumentedPackageScripts(), [], 'missing documented npm scripts'),
|
|
3409
3410
|
},
|
|
3411
|
+
{
|
|
3412
|
+
name: 'CI verifies the supported Node floor and gates npm publishing',
|
|
3413
|
+
run: () => {
|
|
3414
|
+
const testWorkflow = fs.readFileSync(path.join(packageRoot, '.github', 'workflows', 'test.yml'), 'utf8');
|
|
3415
|
+
assertIncludes(testWorkflow, "node-version: ['18', '24']", 'test workflow Node matrix');
|
|
3416
|
+
assertIncludes(testWorkflow, 'run: npm test', 'test workflow suite');
|
|
3417
|
+
assertIncludes(testWorkflow, 'run: npm pack --dry-run', 'test workflow package check');
|
|
3418
|
+
|
|
3419
|
+
const publishWorkflow = fs.readFileSync(path.join(packageRoot, '.github', 'workflows', 'publish-npm.yml'), 'utf8');
|
|
3420
|
+
const testIndex = publishWorkflow.indexOf('run: npm test');
|
|
3421
|
+
const packIndex = publishWorkflow.indexOf('run: npm pack --dry-run');
|
|
3422
|
+
const publishIndex = publishWorkflow.indexOf('run: npm publish');
|
|
3423
|
+
assertEqual(testIndex >= 0 && testIndex < publishIndex, true, 'publish workflow test gate');
|
|
3424
|
+
assertEqual(packIndex >= 0 && packIndex < publishIndex, true, 'publish workflow package gate');
|
|
3425
|
+
assertEqual(pkg.scripts?.['test:openrulebench'], 'node test/run-openrulebench.mjs', 'OpenRuleBench test script');
|
|
3426
|
+
},
|
|
3427
|
+
},
|
|
3410
3428
|
{
|
|
3411
3429
|
name: 'documented conformance totals match the generated report',
|
|
3412
3430
|
run: () => assertArrayEqual(documentedConformanceMetricIssues(), [], 'documented conformance totals'),
|
|
@@ -3856,6 +3874,21 @@ answer(ok) :-
|
|
|
3856
3874
|
assertEqual(result.stdout, 'member(a, "ab").\nmember(b, "ab").\n', 'stdout');
|
|
3857
3875
|
},
|
|
3858
3876
|
},
|
|
3877
|
+
{
|
|
3878
|
+
name: 'JavaScript run autoloads top-level goals for parsed Program instances',
|
|
3879
|
+
run: () => {
|
|
3880
|
+
const program = Program.parse('');
|
|
3881
|
+
const revision = program.revision;
|
|
3882
|
+
program.stratifiedNegation;
|
|
3883
|
+
const result = runEyeProlog(program, { goal: 'member(X,[a,b])' });
|
|
3884
|
+
assertEqual(result.stdout, 'member(a, "ab").\nmember(b, "ab").\n', 'stdout');
|
|
3885
|
+
assertEqual(program.findGroup('member', 2)?.module, 'lists', 'autoloaded Program predicate');
|
|
3886
|
+
assertEqual(program.autoloadedPredicates.map((entry) => `${entry.indicator}:${entry.library}`).join(','),
|
|
3887
|
+
'member/2:lists', 'autoload metadata');
|
|
3888
|
+
assertEqual(program.revision, revision + 1, 'Program revision after autoload');
|
|
3889
|
+
assertEqual(program.stratifiedNegation, true, 'negation metadata recomputed after autoload');
|
|
3890
|
+
},
|
|
3891
|
+
},
|
|
3859
3892
|
{
|
|
3860
3893
|
name: 'JavaScript run can disable top-level goal autoloading',
|
|
3861
3894
|
run: () => {
|
|
@@ -5707,18 +5740,6 @@ function runCli(args, options = {}) {
|
|
|
5707
5740
|
});
|
|
5708
5741
|
}
|
|
5709
5742
|
|
|
5710
|
-
function assertEqual(actual, expected, label) {
|
|
5711
|
-
if (actual !== expected) throw new Error(`${label} mismatch\nexpected: ${format(expected)}\nactual: ${format(actual)}`);
|
|
5712
|
-
}
|
|
5713
|
-
|
|
5714
|
-
function assertIncludes(actual, expected, label) {
|
|
5715
|
-
if (!actual.includes(expected)) throw new Error(`${label} did not include ${format(expected)}\nactual: ${format(actual)}`);
|
|
5716
|
-
}
|
|
5717
|
-
|
|
5718
|
-
function assertNotIncludes(actual, expected, label) {
|
|
5719
|
-
if (String(actual).includes(expected)) throw new Error(`${label} unexpectedly included ${format(expected)}\nactual: ${format(actual)}`);
|
|
5720
|
-
}
|
|
5721
|
-
|
|
5722
5743
|
function arrayDiffMessages(actual, expected, label) {
|
|
5723
5744
|
const messages = [];
|
|
5724
5745
|
const actualSet = new Set(actual);
|
|
@@ -5744,11 +5765,5 @@ function format(value) {
|
|
|
5744
5765
|
}
|
|
5745
5766
|
|
|
5746
5767
|
if (isMainModule(import.meta.url)) {
|
|
5747
|
-
|
|
5748
|
-
try {
|
|
5749
|
-
runRegression(reporter);
|
|
5750
|
-
reporter.totalLine();
|
|
5751
|
-
} catch (_) {
|
|
5752
|
-
process.exit(1);
|
|
5753
|
-
}
|
|
5768
|
+
await runStandalone((reporter) => runRegression(reporter, process.argv[2] ?? null));
|
|
5754
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
|
-
|
|
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
|
}
|
package/test/test-style.mjs
CHANGED
|
@@ -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
|
+
}
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1819,6 +1819,11 @@ additional logical answer.
|
|
|
1819
1819
|
`analyzeNegation`. It returns `stdout`, the solver's numeric `stats`, and a
|
|
1820
1820
|
nullable `haltCode`; it does not write to the process streams.
|
|
1821
1821
|
|
|
1822
|
+
When `run` receives an already parsed `Program`, canonical interop imports
|
|
1823
|
+
needed only by its host-supplied goals are added to that Program before solving,
|
|
1824
|
+
just as they are while source text is parsed. Pass `autoload: false` when the
|
|
1825
|
+
Program must retain only explicitly imported predicates.
|
|
1826
|
+
|
|
1822
1827
|
For applications that inspect or prepare a theory before running it, use
|
|
1823
1828
|
`Program` directly:
|
|
1824
1829
|
|
|
@@ -6302,8 +6307,10 @@ calls to non-profile predicates from otherwise common modules. `--portable`
|
|
|
6302
6307
|
turns those diagnostics into a failing run, making the conservative profile
|
|
6303
6308
|
suitable for continuous integration. `npm run test:interop` executes the same
|
|
6304
6309
|
portable Towers of Hanoi source under EyeProlog, Trealla, and Scryer when those commands
|
|
6305
|
-
are installed
|
|
6306
|
-
|
|
6310
|
+
are installed. Because those are external runtimes, the default `npm test`
|
|
6311
|
+
instead validates all four generated OpenRuleBench source trees and their
|
|
6312
|
+
engine-specific tabling and WFS adaptations without requiring them. The fast
|
|
6313
|
+
structural check is also available directly as `npm run test:openrulebench`.
|
|
6307
6314
|
|
|
6308
6315
|
#### Library notes beyond the interoperability profile
|
|
6309
6316
|
|