eyeprolog 1.6.38 → 1.6.39
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/check-proof.js +1 -0
- package/src/parser.js +30 -15
- package/src/source-expansion.js +17 -0
- package/test/parallel-tasks.mjs +14 -2
- package/test/run-examples.mjs +11 -0
- package/test/run-proof-checking.mjs +2 -1
package/package.json
CHANGED
package/src/check-proof.js
CHANGED
|
@@ -212,6 +212,7 @@ export function readProofDocument(text, program, fail = () => {}) {
|
|
|
212
212
|
doubleQuotes: program?.doubleQuotes ?? 'chars',
|
|
213
213
|
operatorDefinitions: [...(program?.operators?.values() ?? [])],
|
|
214
214
|
sourceMetadata: false,
|
|
215
|
+
generalParser: true,
|
|
215
216
|
});
|
|
216
217
|
} catch (error) {
|
|
217
218
|
fail('C3', `the document does not read: ${error?.message ?? String(error)}`, null);
|
package/src/parser.js
CHANGED
|
@@ -51,10 +51,14 @@ function scanFastRangeToken(text, i, end) {
|
|
|
51
51
|
return i;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// The Unicode property tests below are needed only past ASCII: for a code
|
|
55
|
+
// under 128 the plain code tests give exactly the same answer, and these
|
|
56
|
+
// helpers run once per source character.
|
|
54
57
|
function isWhitespaceCharacter(character) {
|
|
55
58
|
if (!character) return false;
|
|
56
59
|
const code = character.charCodeAt(0);
|
|
57
|
-
|
|
60
|
+
if (code < 128) return isWhitespaceCode(code);
|
|
61
|
+
return /\p{White_Space}/u.test(character);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function isUnicodeUpperCharacter(character) {
|
|
@@ -82,21 +86,24 @@ function isNameContinueCode(code) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
function isNameContinueCharacter(character) {
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
if (!character) return false;
|
|
90
|
+
const code = character.charCodeAt(0);
|
|
91
|
+
if (code < 128) return isNameContinueCode(code);
|
|
92
|
+
return isUnicodeNameContinueCharacter(character);
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
function isVariableStartCharacter(character) {
|
|
90
96
|
if (!character) return false;
|
|
91
97
|
const code = character.charCodeAt(0);
|
|
92
|
-
return code === 95 || (code >= 65 && code <= 90)
|
|
98
|
+
if (code < 128) return code === 95 || (code >= 65 && code <= 90);
|
|
99
|
+
return isUnicodeUpperCharacter(character);
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
function isPlainAtomStartCharacter(character) {
|
|
96
103
|
if (!character) return false;
|
|
97
104
|
const code = character.charCodeAt(0);
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
if (code < 128) return code >= 97 && code <= 122;
|
|
106
|
+
return isUnicodeLetterCharacter(character) && !isUnicodeUpperCharacter(character);
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
const graphicAtomChars = '#$&*+-./<=>?@^~\\:';
|
|
@@ -311,6 +318,11 @@ function isCodePointOutOfRange(code) {
|
|
|
311
318
|
return code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff);
|
|
312
319
|
}
|
|
313
320
|
|
|
321
|
+
const PUNCTUATION_TOKENS = new Map([
|
|
322
|
+
['(', TOK.LPAREN], [')', TOK.RPAREN], ['[', TOK.LBRACKET], [']', TOK.RBRACKET],
|
|
323
|
+
['{', TOK.LBRACE], ['}', TOK.RBRACE], [',', TOK.COMMA], ['|', TOK.BAR], ['.', TOK.DOT],
|
|
324
|
+
]);
|
|
325
|
+
|
|
314
326
|
class Parser {
|
|
315
327
|
constructor(source, options = {}) {
|
|
316
328
|
this.source = String(source ?? '');
|
|
@@ -368,6 +380,7 @@ class Parser {
|
|
|
368
380
|
return ch;
|
|
369
381
|
}
|
|
370
382
|
convertedSlice(start, end) {
|
|
383
|
+
if (this.parserFlagState.charConversions.size === 0) return this.source.slice(start, end);
|
|
371
384
|
let out = '';
|
|
372
385
|
for (const ch of this.source.slice(start, end)) out += this.convertCharacter(ch);
|
|
373
386
|
return out;
|
|
@@ -614,13 +627,10 @@ class Parser {
|
|
|
614
627
|
return { type: TOK.ATOM, text: ';', line };
|
|
615
628
|
}
|
|
616
629
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
'{': TOK.LBRACE, '}': TOK.RBRACE, ',': TOK.COMMA, '|': TOK.BAR, '.': TOK.DOT,
|
|
620
|
-
};
|
|
621
|
-
if (punct[ch]) {
|
|
630
|
+
const punctuation = PUNCTUATION_TOKENS.get(ch);
|
|
631
|
+
if (punctuation) {
|
|
622
632
|
this.take();
|
|
623
|
-
return { type:
|
|
633
|
+
return { type: punctuation, text: ch, line, precededByLayout };
|
|
624
634
|
}
|
|
625
635
|
if (ch === ':' && this.peek(1) === '-' &&
|
|
626
636
|
!(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
|
|
@@ -671,7 +681,9 @@ class Parser {
|
|
|
671
681
|
// A signed numeric literal is only recognized where a term may start.
|
|
672
682
|
// Otherwise the minus is the standard infix operator, so compact ISO
|
|
673
683
|
// syntax such as `X-1` must not be read as `X` followed by `-1`.
|
|
674
|
-
|
|
684
|
+
// Whether the previous token ends a term matters only for a minus sign
|
|
685
|
+
// before a digit, so it is decided only then.
|
|
686
|
+
const previousEndsTerm = () => this.previousToken && (
|
|
675
687
|
[TOK.VAR, TOK.NUMBER, TOK.STRING, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE].includes(this.previousToken.type) ||
|
|
676
688
|
(this.previousToken.type === TOK.ATOM &&
|
|
677
689
|
(this.postfixOperators.has(this.previousToken.text) ||
|
|
@@ -679,7 +691,7 @@ class Parser {
|
|
|
679
691
|
!this.prefixOperators.has(this.previousToken.text))))
|
|
680
692
|
);
|
|
681
693
|
if (isDigitCode(ch.charCodeAt(0)) ||
|
|
682
|
-
(ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm)) {
|
|
694
|
+
(ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm())) {
|
|
683
695
|
const start = this.pos;
|
|
684
696
|
const negative = this.peek() === '-';
|
|
685
697
|
if (negative) this.take();
|
|
@@ -1435,7 +1447,10 @@ export function parseClauses(source, options = {}) {
|
|
|
1435
1447
|
const parserOptions = ownsParserFlagState
|
|
1436
1448
|
? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes, charConversion: 'on', charConversions: new Map() } }
|
|
1437
1449
|
: options;
|
|
1438
|
-
|
|
1450
|
+
// `generalParser` reads with the general parser from the start: a text of
|
|
1451
|
+
// large, nested terms, such as a proof document, gains nothing from the
|
|
1452
|
+
// line-oriented fast path and pays for a parser per term in it.
|
|
1453
|
+
if (options.sourceMetadata === false && options.readTermEnd == null && options.generalParser !== true) {
|
|
1439
1454
|
const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
|
|
1440
1455
|
if (clauses) return clauses;
|
|
1441
1456
|
if (ownsParserFlagState) {
|
package/src/source-expansion.js
CHANGED
|
@@ -62,10 +62,27 @@ function copyHookResult(term, env, inputNames) {
|
|
|
62
62
|
return copy(term);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Whether some clause of a hook could unify with `input` as its first
|
|
66
|
+
// argument. A clause whose first argument has another type, name or arity
|
|
67
|
+
// cannot, so when none can the hook does not run at all: a library such as
|
|
68
|
+
// CLP(Z) expands only its own goals, and asking a fresh solver about every
|
|
69
|
+
// other clause and goal of a program cost more than loading it.
|
|
70
|
+
function hookMayApply(group, input) {
|
|
71
|
+
if (input?.type === VAR) return true;
|
|
72
|
+
for (const clause of group.clauses) {
|
|
73
|
+
const first = clause.head?.args?.[0];
|
|
74
|
+
if (first == null || first.type === VAR) return true;
|
|
75
|
+
if (first.type !== input.type || first.name !== input.name) continue;
|
|
76
|
+
if (first.type !== COMPOUND || first.args.length === input.args.length) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
function runHook(program, module, name, input, loadContext = null) {
|
|
66
82
|
for (const owner of hookModules(module)) {
|
|
67
83
|
const group = exactHookGroup(program, owner, name);
|
|
68
84
|
if (group == null || group.clauses.length === 0) continue;
|
|
85
|
+
if (!hookMayApply(group, input)) continue;
|
|
69
86
|
|
|
70
87
|
const output = variable(`\u0000source-expansion:${++expansionFresh}`);
|
|
71
88
|
const goal = compound(name, [input, output]);
|
package/test/parallel-tasks.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// A small ordered worker pool for the suites that do the same work once per
|
|
2
2
|
// file: run every example, prove every example, check every packaged proof.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Tasks are handed to workers most expensive first when the caller can
|
|
5
|
+
// estimate a task's cost: a suite whose slowest task starts late spends its
|
|
6
|
+
// last seconds running that one task alone. Results are still reported in task
|
|
7
|
+
// order however the workers finish, so a parallel
|
|
5
8
|
// suite numbers its tests exactly as the sequential one did and a failure still
|
|
6
9
|
// stops the run at the same test. Each suite compares inside its worker, so
|
|
7
10
|
// only a name, an elapsed time, an optional failure, and a small plain-data
|
|
@@ -17,6 +20,7 @@ export async function runTasksInParallel({
|
|
|
17
20
|
maxWorkers,
|
|
18
21
|
runLocally,
|
|
19
22
|
onResult,
|
|
23
|
+
cost = null,
|
|
20
24
|
}) {
|
|
21
25
|
if (names.length === 0) return;
|
|
22
26
|
const parallelism = os.availableParallelism?.() ?? os.cpus().length;
|
|
@@ -35,6 +39,13 @@ export async function runTasksInParallel({
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
const completedResults = new Map();
|
|
42
|
+
// Dispatch order: task indexes by descending estimated cost, ties in task
|
|
43
|
+
// order. Reporting still follows the task order itself.
|
|
44
|
+
const dispatch = names.map((_, index) => index);
|
|
45
|
+
if (typeof cost === 'function') {
|
|
46
|
+
const costs = names.map((name) => cost(name));
|
|
47
|
+
dispatch.sort((left, right) => costs[right] - costs[left] || left - right);
|
|
48
|
+
}
|
|
38
49
|
let nextTask = 0;
|
|
39
50
|
let nextReport = 0;
|
|
40
51
|
let completed = 0;
|
|
@@ -72,7 +83,8 @@ export async function runTasksInParallel({
|
|
|
72
83
|
|
|
73
84
|
const assign = (worker) => {
|
|
74
85
|
if (nextTask >= names.length || settled) return;
|
|
75
|
-
|
|
86
|
+
const id = dispatch[nextTask++];
|
|
87
|
+
worker.postMessage({ ...message, id, name: names[id] });
|
|
76
88
|
};
|
|
77
89
|
|
|
78
90
|
for (const worker of workers) {
|
package/test/run-examples.mjs
CHANGED
|
@@ -50,6 +50,16 @@ const exampleTaskKinds = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
// A packaged proof grows with the derivation it records, so its size is a
|
|
54
|
+
// cheap estimate of how long an example takes to run, prove, and check.
|
|
55
|
+
export function exampleCost(name) {
|
|
56
|
+
try {
|
|
57
|
+
return fs.statSync(path.join(expectedProofDir, name)).size;
|
|
58
|
+
} catch {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
53
63
|
function runExampleTasks(names, kind, reporter) {
|
|
54
64
|
return runTasksInParallel({
|
|
55
65
|
names,
|
|
@@ -57,6 +67,7 @@ function runExampleTasks(names, kind, reporter) {
|
|
|
57
67
|
workerData: { exampleWorker: true },
|
|
58
68
|
message: { kind },
|
|
59
69
|
maxWorkers: 3,
|
|
70
|
+
cost: exampleCost,
|
|
60
71
|
runLocally: (name) => exampleTaskKinds[kind](name),
|
|
61
72
|
onResult: (name, result) => reporter.testResult(name, result),
|
|
62
73
|
});
|
|
@@ -15,7 +15,7 @@ import { isMainThread, parentPort, workerData } from 'node:worker_threads';
|
|
|
15
15
|
import { Program } from '../src/index.js';
|
|
16
16
|
import { checkProofDocument, checkReportTerms, sourceGoals, verdict } from '../src/check-proof.js';
|
|
17
17
|
import { TestReporter, assertEqual, isMainModule, runStandalone } from './test-style.mjs';
|
|
18
|
-
import { proofExamples } from './run-examples.mjs';
|
|
18
|
+
import { exampleCost, proofExamples } from './run-examples.mjs';
|
|
19
19
|
import { runTasksInParallel, serveTasks } from './parallel-tasks.mjs';
|
|
20
20
|
|
|
21
21
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -37,6 +37,7 @@ export async function runProofChecking(reporter = new TestReporter()) {
|
|
|
37
37
|
workerUrl: new URL(import.meta.url),
|
|
38
38
|
workerData: { proofCheckingWorker: true },
|
|
39
39
|
maxWorkers: 3,
|
|
40
|
+
cost: exampleCost,
|
|
40
41
|
runLocally: (name) => checkPackagedProof(name),
|
|
41
42
|
onResult: (name, result) => {
|
|
42
43
|
for (const field of Object.keys(totals)) totals[field] += result.value?.[field] ?? 0;
|