eyeprolog 1.3.87 → 1.3.89
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 +1 -1
- package/src/ARCHITECTURE.md +3 -1
- package/src/iso.js +13 -0
- package/src/term.js +19 -5
- package/test/run-all.mjs +1 -1
- package/test/run-examples.mjs +1 -7
- package/test/run-regression.mjs +210 -5
- package/test/test-style.mjs +2 -2
- package/the-art-of-eyeprolog.md +6 -0
package/package.json
CHANGED
package/src/ARCHITECTURE.md
CHANGED
|
@@ -36,7 +36,9 @@ Annotations are persistent across `Env.clone()` and therefore backtrack with the
|
|
|
36
36
|
substitution. Language services may attach immutable constraint descriptors,
|
|
37
37
|
but the unconstrained unification hot path only performs a null check; descriptor
|
|
38
38
|
validation and reindexing run only in environments that actually contain
|
|
39
|
-
annotations.
|
|
39
|
+
annotations. Descriptors can optionally define logical subsumption so the
|
|
40
|
+
store retains only its strongest pending constraints. `dif/2` is the first
|
|
41
|
+
descriptor-based user of this mechanism.
|
|
40
42
|
|
|
41
43
|
`atts.js` layers Prolog-visible attributed variables over that same persistent
|
|
42
44
|
environment. Per-module attributes follow the current variable representative.
|
package/src/iso.js
CHANGED
|
@@ -211,6 +211,19 @@ function difConstraint(left, right) {
|
|
|
211
211
|
skipAttributeHooks: true,
|
|
212
212
|
}) ? 'pending' : 'entailed';
|
|
213
213
|
},
|
|
214
|
+
subsumes(other, env) {
|
|
215
|
+
if (other?.kind !== 'dif') return false;
|
|
216
|
+
// dif(A,B) implies dif(C,D) exactly when C=D entails A=B. Apply the
|
|
217
|
+
// most general finite-tree unifier for the other's forbidden equality;
|
|
218
|
+
// if this equality makes our terms identical, every instance forbidden
|
|
219
|
+
// by the other constraint is already forbidden by this one.
|
|
220
|
+
const probe = env.clone();
|
|
221
|
+
if (!unify(other.left, other.right, probe, {
|
|
222
|
+
skipVariableConstraints: true,
|
|
223
|
+
skipAttributeHooks: true,
|
|
224
|
+
})) return true;
|
|
225
|
+
return identical(left, right, probe);
|
|
226
|
+
},
|
|
214
227
|
residualGoal() {
|
|
215
228
|
return compound('dif', [left, right]);
|
|
216
229
|
},
|
package/src/term.js
CHANGED
|
@@ -465,9 +465,24 @@ export class Env {
|
|
|
465
465
|
typeof constraint.status !== 'function') {
|
|
466
466
|
throw new TypeError('variable constraint requires variables(env) and status(env)');
|
|
467
467
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
468
|
+
this._setNormalizedVariableConstraints([...(this._variableConstraints ?? []), constraint]);
|
|
469
|
+
}
|
|
470
|
+
_setNormalizedVariableConstraints(constraints) {
|
|
471
|
+
// Descriptors may define logical subsumption. Keep the strongest pending
|
|
472
|
+
// constraints so equivalent, symmetric, or weaker residual goals do not
|
|
473
|
+
// accumulate, while unrelated descriptor kinds remain untouched.
|
|
474
|
+
const normalized = [];
|
|
475
|
+
candidateLoop:
|
|
476
|
+
for (const candidate of constraints) {
|
|
477
|
+
for (const existing of normalized) {
|
|
478
|
+
if (existing === candidate || existing.subsumes?.(candidate, this) === true) continue candidateLoop;
|
|
479
|
+
}
|
|
480
|
+
for (let index = normalized.length - 1; index >= 0; index--) {
|
|
481
|
+
if (candidate.subsumes?.(normalized[index], this) === true) normalized.splice(index, 1);
|
|
482
|
+
}
|
|
483
|
+
normalized.push(candidate);
|
|
484
|
+
}
|
|
485
|
+
this._variableConstraints = normalized.length === 0 ? null : new Set(normalized);
|
|
471
486
|
this._reindexVariableConstraints();
|
|
472
487
|
}
|
|
473
488
|
variableConstraints(kind = null) {
|
|
@@ -487,8 +502,7 @@ export class Env {
|
|
|
487
502
|
if (status === 'violated') return false;
|
|
488
503
|
if (status !== 'entailed') pending.add(constraint);
|
|
489
504
|
}
|
|
490
|
-
this.
|
|
491
|
-
this._reindexVariableConstraints();
|
|
505
|
+
this._setNormalizedVariableConstraints(pending);
|
|
492
506
|
return true;
|
|
493
507
|
}
|
|
494
508
|
_reindexVariableConstraints() {
|
package/test/run-all.mjs
CHANGED
|
@@ -21,7 +21,7 @@ await runStandalone(async (reporter) => {
|
|
|
21
21
|
runOpenRuleBenchChecks(reporter);
|
|
22
22
|
runArchitecture(reporter);
|
|
23
23
|
runCleanup(reporter);
|
|
24
|
-
runRegression(reporter);
|
|
24
|
+
await runRegression(reporter);
|
|
25
25
|
await runPlayground(reporter);
|
|
26
26
|
await runExamples(reporter);
|
|
27
27
|
runBookExamples(reporter);
|
package/test/run-examples.mjs
CHANGED
|
@@ -87,13 +87,7 @@ export async function runExamples(reporter = new TestReporter()) {
|
|
|
87
87
|
.sort();
|
|
88
88
|
|
|
89
89
|
reporter.section('Examples');
|
|
90
|
-
|
|
91
|
-
if (deepTaxonomyIndex >= 0) {
|
|
92
|
-
await runExampleTasks(files.slice(0, deepTaxonomyIndex + 1), (name, result) => reporter.testResult(name, result), 2);
|
|
93
|
-
await runExampleTasks(files.slice(deepTaxonomyIndex + 1), (name, result) => reporter.testResult(name, result), 3);
|
|
94
|
-
} else {
|
|
95
|
-
await runExampleTasks(files, (name, result) => reporter.testResult(name, result), 3);
|
|
96
|
-
}
|
|
90
|
+
await runExampleTasks(files, (name, result) => reporter.testResult(name, result), 3);
|
|
97
91
|
reporter.sectionTotal('examples');
|
|
98
92
|
|
|
99
93
|
reporter.section('Proof examples');
|
package/test/run-regression.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { Worker, parentPort, workerData } from 'node:worker_threads';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import publicDefaultApi from '../index.js';
|
|
12
13
|
import * as publicApi from '../src/index.js';
|
|
@@ -84,13 +85,15 @@ function sourceAtom(value) {
|
|
|
84
85
|
return `'${String(value).replaceAll('\\', '\\\\').replaceAll("'", "''")}'`;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
const sections = [
|
|
88
|
+
const regressionSections = [
|
|
89
89
|
{ key: 'regression', name: 'Regression', cases: regressionCases },
|
|
90
90
|
{ key: 'docs', name: 'Documentation sync', cases: documentationSyncCases },
|
|
91
91
|
{ key: 'api', name: 'API', cases: apiCases },
|
|
92
92
|
{ key: 'white-box', name: 'White-box', cases: whiteBoxCases },
|
|
93
|
-
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
export async function runRegression(reporter = new TestReporter(), requestedSection = null) {
|
|
96
|
+
const sections = regressionSections;
|
|
94
97
|
const selected = requestedSection == null
|
|
95
98
|
? sections
|
|
96
99
|
: sections.filter((section) => section.key === requestedSection);
|
|
@@ -98,16 +101,167 @@ export function runRegression(reporter = new TestReporter(), requestedSection =
|
|
|
98
101
|
throw new Error(`unknown regression section: ${requestedSection}; expected ${sections.map(({ key }) => key).join(', ')}`);
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
const prepared = selected.map((section) => ({ ...section, tests: section.cases() }));
|
|
105
|
+
const testCount = prepared.reduce((sum, section) => sum + section.tests.length, 0);
|
|
106
|
+
const parallelism = os.availableParallelism?.() ?? os.cpus().length;
|
|
107
|
+
// Several cases start bounded-heap child processes, so leave CPU and memory
|
|
108
|
+
// headroom instead of matching the host's full logical-core count.
|
|
109
|
+
const workerCount = Math.min(testCount, Math.max(1, Math.min(4, parallelism - 1)));
|
|
110
|
+
if (workerCount === 1) {
|
|
111
|
+
withRegressionTemp(() => {
|
|
112
|
+
for (const section of prepared) runSection(reporter, section.name, section.tests);
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const results = new Map();
|
|
118
|
+
let reportSection = 0;
|
|
119
|
+
let reportTest = 0;
|
|
120
|
+
reporter.section(prepared[0].name);
|
|
121
|
+
const reportReady = () => {
|
|
122
|
+
while (reportSection < prepared.length) {
|
|
123
|
+
const section = prepared[reportSection];
|
|
124
|
+
const result = results.get(testKey(section.key, reportTest));
|
|
125
|
+
if (result == null) return;
|
|
126
|
+
reporter.testResult(section.tests[reportTest].name, result);
|
|
127
|
+
reportTest++;
|
|
128
|
+
if (reportTest !== section.tests.length) continue;
|
|
129
|
+
reporter.sectionTotal(sectionLabel(section.name), sectionElapsedMs(section, results));
|
|
130
|
+
reportSection++;
|
|
131
|
+
reportTest = 0;
|
|
132
|
+
if (reportSection < prepared.length) reporter.section(prepared[reportSection].name);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
await runRegressionWorkers(prepared, workerCount, results, reportReady);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function withRegressionTemp(runTests) {
|
|
101
139
|
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-regression.'));
|
|
102
140
|
tmpCounter = 0;
|
|
103
141
|
try {
|
|
104
|
-
|
|
142
|
+
return runTests();
|
|
105
143
|
} finally {
|
|
106
144
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
107
145
|
tmp = null;
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
148
|
|
|
149
|
+
async function runRegressionWorkers(sections, workerCount, results, onResult) {
|
|
150
|
+
const tasks = sections.flatMap((section) => section.tests.map((_, index) => ({
|
|
151
|
+
sectionKey: section.key,
|
|
152
|
+
index,
|
|
153
|
+
})));
|
|
154
|
+
let nextTask = 0;
|
|
155
|
+
let completed = 0;
|
|
156
|
+
let settled = false;
|
|
157
|
+
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
const workers = Array.from({ length: workerCount }, () => new Worker(new URL(import.meta.url), {
|
|
160
|
+
workerData: { regressionWorker: true },
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
const closeWorkers = () => {
|
|
164
|
+
for (const worker of workers) worker.postMessage({ close: true });
|
|
165
|
+
};
|
|
166
|
+
const fail = (error) => {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
Promise.all(workers.map((worker) => worker.terminate())).finally(() => reject(error));
|
|
170
|
+
};
|
|
171
|
+
const assign = (worker) => {
|
|
172
|
+
if (settled) return;
|
|
173
|
+
const task = tasks[nextTask++];
|
|
174
|
+
if (task != null) worker.postMessage(task);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
for (const worker of workers) {
|
|
178
|
+
worker.on('message', (message) => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
if (message.ready) {
|
|
181
|
+
assign(worker);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
results.set(testKey(message.sectionKey, message.index), {
|
|
185
|
+
ms: message.ms,
|
|
186
|
+
error: deserializeWorkerError(message.error),
|
|
187
|
+
startedAt: message.startedAt,
|
|
188
|
+
finishedAt: message.finishedAt,
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
onResult();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
fail(error);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
completed++;
|
|
197
|
+
if (completed === tasks.length) {
|
|
198
|
+
settled = true;
|
|
199
|
+
const exits = Promise.all(workers.map((candidate) => new Promise((done, failed) => {
|
|
200
|
+
candidate.once('exit', done);
|
|
201
|
+
candidate.once('error', failed);
|
|
202
|
+
})));
|
|
203
|
+
closeWorkers();
|
|
204
|
+
exits.then(() => resolve(results), reject);
|
|
205
|
+
} else {
|
|
206
|
+
assign(worker);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
worker.on('error', fail);
|
|
210
|
+
worker.on('exit', (code) => {
|
|
211
|
+
if (!settled) fail(new Error(`regression worker exited unexpectedly with status ${code}`));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function testKey(sectionKey, index) {
|
|
218
|
+
return `${sectionKey}:${index}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sectionElapsedMs(section, results) {
|
|
222
|
+
const timings = section.tests.map((_, index) => results.get(testKey(section.key, index)));
|
|
223
|
+
return Math.max(...timings.map(({ finishedAt }) => finishedAt)) -
|
|
224
|
+
Math.min(...timings.map(({ startedAt }) => startedAt));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function deserializeWorkerError(error) {
|
|
228
|
+
if (error == null) return null;
|
|
229
|
+
return Object.assign(new Error(error.message), { stack: error.stack });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function startRegressionWorker() {
|
|
233
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeprolog-regression-worker.'));
|
|
234
|
+
tmpCounter = 0;
|
|
235
|
+
const casesBySection = new Map(regressionSections.map((section) => [section.key, section.cases()]));
|
|
236
|
+
parentPort.on('message', ({ sectionKey, index, close }) => {
|
|
237
|
+
if (close) {
|
|
238
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
239
|
+
tmp = null;
|
|
240
|
+
parentPort.close();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const testCase = casesBySection.get(sectionKey)?.[index];
|
|
244
|
+
const startedAt = Date.now();
|
|
245
|
+
const started = performance.now();
|
|
246
|
+
let error = null;
|
|
247
|
+
try {
|
|
248
|
+
if (testCase == null) throw new Error(`unknown regression test ${sectionKey}:${index}`);
|
|
249
|
+
testCase.run();
|
|
250
|
+
} catch (caught) {
|
|
251
|
+
error = { message: caught?.message ?? String(caught), stack: caught?.stack ?? String(caught) };
|
|
252
|
+
}
|
|
253
|
+
parentPort.postMessage({
|
|
254
|
+
sectionKey,
|
|
255
|
+
index,
|
|
256
|
+
ms: Math.round(performance.now() - started),
|
|
257
|
+
startedAt,
|
|
258
|
+
finishedAt: Date.now(),
|
|
259
|
+
error,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
parentPort.postMessage({ ready: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
111
265
|
function regressionCases() {
|
|
112
266
|
return [
|
|
113
267
|
{
|
|
@@ -210,6 +364,55 @@ function regressionCases() {
|
|
|
210
364
|
'freeze/2 residual projection terminates alongside dif/2 (issue #73)');
|
|
211
365
|
},
|
|
212
366
|
},
|
|
367
|
+
{
|
|
368
|
+
name: 'dif/2 removes symmetric and logically weaker residual constraints (issue #79)',
|
|
369
|
+
run: () => {
|
|
370
|
+
const residuals = runCli([], {
|
|
371
|
+
input:
|
|
372
|
+
'dif(X,Y),dif(Y,X).\n' +
|
|
373
|
+
'dif(X,Y),dif(X-Y,Z-Z).\n' +
|
|
374
|
+
'dif(X-Y,Z-Z),dif(X,Y).\n' +
|
|
375
|
+
'dif(X,a),dif(X,b).\n' +
|
|
376
|
+
'dif(X,a),dif(Y,a),X=Y.\n' +
|
|
377
|
+
'halt.\n',
|
|
378
|
+
});
|
|
379
|
+
assertEqual(residuals.status, 0, 'dif/2 normalization display status');
|
|
380
|
+
assertEqual(residuals.stdout,
|
|
381
|
+
'?- dif(X, Y).\n' +
|
|
382
|
+
'?- dif(X, Y).\n' +
|
|
383
|
+
'?- dif(X, Y).\n' +
|
|
384
|
+
'?- dif(X, a), dif(X, b).\n' +
|
|
385
|
+
'?- X = Y, dif(Y, a).\n' +
|
|
386
|
+
'?- ',
|
|
387
|
+
'dif/2 residuals are minimal independent of insertion order');
|
|
388
|
+
assertEqual(residuals.stderr, '', 'dif/2 normalization display stderr');
|
|
389
|
+
|
|
390
|
+
const pendingDifCount = (text) => {
|
|
391
|
+
const solver = new Solver(Program.parse(''), { registry: getEyePrologRegistry() });
|
|
392
|
+
const solutions = solver.solve([parseGoalText(text)], new Env(), 0);
|
|
393
|
+
const answer = solutions.next();
|
|
394
|
+
assertEqual(answer.done, false, `${text} succeeds`);
|
|
395
|
+
const count = answer.value.variableConstraints('dif').length;
|
|
396
|
+
solutions.return();
|
|
397
|
+
return count;
|
|
398
|
+
};
|
|
399
|
+
assertEqual(pendingDifCount('dif(X,Y),dif(Y,X)'), 1,
|
|
400
|
+
'symmetric disequalities share one descriptor');
|
|
401
|
+
assertEqual(pendingDifCount('dif(X-Y,Z-Z),dif(X,Y)'), 1,
|
|
402
|
+
'stronger later disequality replaces the weaker descriptor');
|
|
403
|
+
assertEqual(pendingDifCount('dif(X,a),dif(X,b)'), 2,
|
|
404
|
+
'independent disequalities are both retained');
|
|
405
|
+
assertEqual(pendingDifCount('dif(X,a),dif(Y,a),X=Y'), 1,
|
|
406
|
+
'later aliasing coalesces newly equivalent disequalities');
|
|
407
|
+
for (const prefix of [
|
|
408
|
+
'dif(X,Y),dif(X-Y,Z-Z)',
|
|
409
|
+
'dif(X-Y,Z-Z),dif(X,Y)',
|
|
410
|
+
]) {
|
|
411
|
+
assertEqual(run('', { goal: `${prefix},X=Y` }).stats.completed_goal_lists, 0,
|
|
412
|
+
`${prefix} retains the stronger disequality semantics`);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
},
|
|
213
416
|
{
|
|
214
417
|
name: 'library(atts) provides Scryer-style attributed-variable hooks',
|
|
215
418
|
run: () => {
|
|
@@ -7136,6 +7339,8 @@ function format(value) {
|
|
|
7136
7339
|
return typeof value === 'string' ? JSON.stringify(value) : String(value);
|
|
7137
7340
|
}
|
|
7138
7341
|
|
|
7139
|
-
if (
|
|
7342
|
+
if (workerData?.regressionWorker) {
|
|
7343
|
+
startRegressionWorker();
|
|
7344
|
+
} else if (isMainModule(import.meta.url)) {
|
|
7140
7345
|
await runStandalone((reporter) => runRegression(reporter, process.argv[2] ?? null));
|
|
7141
7346
|
}
|
package/test/test-style.mjs
CHANGED
|
@@ -41,12 +41,12 @@ export class TestReporter {
|
|
|
41
41
|
this.sectionCount++;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
sectionTotal(label = null) {
|
|
44
|
+
sectionTotal(label = null, elapsedMs = null) {
|
|
45
45
|
if (this.currentSection == null) return;
|
|
46
46
|
|
|
47
47
|
const ok = this.ok - this.currentSection.okAtStart;
|
|
48
48
|
const total = this.total - this.currentSection.totalAtStart;
|
|
49
|
-
const ms = nowMs() - this.currentSection.startedAt;
|
|
49
|
+
const ms = elapsedMs ?? nowMs() - this.currentSection.startedAt;
|
|
50
50
|
const suite = label ?? defaultSectionLabel(this.currentSection.name);
|
|
51
51
|
this.stdout.write(`${colors.green}OK${colors.reset} ${ok}/${total} ${suite} tests passed ${colors.dim}(${ms} ms)${colors.reset}\n`);
|
|
52
52
|
}
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6284,6 +6284,12 @@ otherwise it delays the goal until `Term` becomes nonvariable. Suspensions are
|
|
|
6284
6284
|
kept in the logical environment, so bindings and backtracking remain isolated
|
|
6285
6285
|
between solution branches.
|
|
6286
6286
|
|
|
6287
|
+
`dif(?Left,?Right)` posts a delayed finite-tree disequality when its arguments
|
|
6288
|
+
can still unify. Its residual store is normalized by logical implication:
|
|
6289
|
+
symmetric or equivalent constraints share one residual, and a stronger
|
|
6290
|
+
constraint removes weaker ones regardless of insertion order. Independent
|
|
6291
|
+
disequalities remain separate.
|
|
6292
|
+
|
|
6287
6293
|
<!-- eyeprolog-library-catalog:start -->
|
|
6288
6294
|
|
|
6289
6295
|
| Module | Exported predicate indicators | Primary role |
|