eyeprolog 1.6.37 → 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/examples/output/bayes-therapy.pl +1 -1
- package/package.json +1 -1
- package/src/check-proof.js +1 -0
- package/src/explain.js +100 -5
- package/src/parser.js +96 -37
- package/src/program.js +13 -0
- package/src/proof-document.js +2 -1
- package/src/source-expansion.js +17 -0
- package/src/write.js +11 -0
- package/test/conformance/cases/arithmetic/024_numeric_literal_readback.pl +1 -1
- package/test/conformance/expected/arithmetic/024_numeric_literal_readback.pl +2 -2
- package/test/conformance/expected/terms/003_terms_and_readback.pl +1 -1
- package/test/parallel-tasks.mjs +14 -2
- package/test/regression/cases-regression.mjs +10 -15
- package/test/run-examples.mjs +11 -0
- package/test/run-proof-checking.mjs +2 -1
|
@@ -10,7 +10,7 @@ expectedSuccess(oseltamivir, 0.28514101258814745).
|
|
|
10
10
|
expectedSuccess(antihistamine, 0.10026891936485297).
|
|
11
11
|
expectedSuccess(antibiotic, 0.1109525797960936).
|
|
12
12
|
expectedSuccess(supportiveCare, 0.2915119539701381).
|
|
13
|
-
expectedAdverse(paxlovid, 0.
|
|
13
|
+
expectedAdverse(paxlovid, 0.1).
|
|
14
14
|
expectedAdverse(oseltamivir, 0.08).
|
|
15
15
|
expectedAdverse(antihistamine, 0.03).
|
|
16
16
|
expectedAdverse(antibiotic, 0.07).
|
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/explain.js
CHANGED
|
@@ -54,6 +54,10 @@ let liveSolver = null;
|
|
|
54
54
|
// sets an explicit `maxDepth` gets that depth as a hard limit instead.
|
|
55
55
|
let chainBeyondDepth = true;
|
|
56
56
|
|
|
57
|
+
// Proofs of ground goals already found while explaining one document, keyed by
|
|
58
|
+
// module and goal; null outside such a document.
|
|
59
|
+
let groundProofs = null;
|
|
60
|
+
|
|
57
61
|
function hostSolver(program, registry) {
|
|
58
62
|
return liveSolver && liveSolver.program === program ? liveSolver : new Solver(program, { registry });
|
|
59
63
|
}
|
|
@@ -177,6 +181,23 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
177
181
|
}
|
|
178
182
|
return;
|
|
179
183
|
}
|
|
184
|
+
// A goal that is already ground binds nothing when proved, so which of its
|
|
185
|
+
// proofs is used cannot matter to the goals after it. Within one proof
|
|
186
|
+
// document its first proof is therefore recorded once and reused: a run
|
|
187
|
+
// whose answers share subderivations -- tabled trajectories, closures --
|
|
188
|
+
// no longer replays the shared part once per answer. This stays inside one
|
|
189
|
+
// generator: a replay nests a generator per level, and one more frame per
|
|
190
|
+
// level would overflow the host stack on deep derivations.
|
|
191
|
+
const memoKey = groundProofs && termIsGround(goal, env)
|
|
192
|
+
? `${group.module}\u0000${termToString(goal, env, true)}`
|
|
193
|
+
: null;
|
|
194
|
+
if (memoKey) {
|
|
195
|
+
const known = groundProofs.get(memoKey);
|
|
196
|
+
if (known) {
|
|
197
|
+
yield { env, node: known };
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
180
201
|
// Explanation replay does not use the solver's answer tables, so its cycle
|
|
181
202
|
// guard applies even when normal execution tables this predicate.
|
|
182
203
|
if (activeVariant(goal, env, active)) return;
|
|
@@ -199,7 +220,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
199
220
|
const substitutions = collectClauseSubstitutions(clause, freshHead, freshBody);
|
|
200
221
|
|
|
201
222
|
if (freshBody.length === 0) {
|
|
202
|
-
|
|
223
|
+
const proved = {
|
|
203
224
|
env: next,
|
|
204
225
|
node: {
|
|
205
226
|
raw: goal,
|
|
@@ -215,6 +236,11 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
215
236
|
children: [],
|
|
216
237
|
},
|
|
217
238
|
};
|
|
239
|
+
if (memoKey) {
|
|
240
|
+
yield rememberGroundProof(memoKey, proved);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
yield proved;
|
|
218
244
|
continue;
|
|
219
245
|
}
|
|
220
246
|
|
|
@@ -224,7 +250,7 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
224
250
|
for (const proved of proveGoalsAll(program, freshBody, next, depth + 1, maxDepth, registry, active, detail)) {
|
|
225
251
|
active.pop();
|
|
226
252
|
activePushed = false;
|
|
227
|
-
|
|
253
|
+
const result = {
|
|
228
254
|
env: proved.env,
|
|
229
255
|
node: {
|
|
230
256
|
raw: goal,
|
|
@@ -237,6 +263,11 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active, de
|
|
|
237
263
|
children: proved.children,
|
|
238
264
|
},
|
|
239
265
|
};
|
|
266
|
+
if (memoKey) {
|
|
267
|
+
yield rememberGroundProof(memoKey, result);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
yield result;
|
|
240
271
|
active.push({ goal, env });
|
|
241
272
|
activePushed = true;
|
|
242
273
|
}
|
|
@@ -472,6 +503,59 @@ function sourceMethod(clause, kind) {
|
|
|
472
503
|
};
|
|
473
504
|
}
|
|
474
505
|
|
|
506
|
+
// Built-in and library steps whose replay has no effect beyond its bindings:
|
|
507
|
+
// arithmetic, comparison, term inspection, and pure list relations. Anything
|
|
508
|
+
// else -- I/O, database updates, constraint stores, generated names -- keeps a
|
|
509
|
+
// derivation from being reused across answers.
|
|
510
|
+
const PURE_REPLAY_STEPS = new Set([
|
|
511
|
+
'true/0', 'is/2', '=:=/2', '=\\=/2', '</2', '>/2', '=</2', '>=/2',
|
|
512
|
+
'=/2', '\\=/2', '==/2', '\\==/2', '@</2', '@>/2', '@=</2', '@>=/2', 'compare/3',
|
|
513
|
+
'var/1', 'nonvar/1', 'atom/1', 'number/1', 'integer/1', 'float/1', 'atomic/1',
|
|
514
|
+
'compound/1', 'callable/1', 'ground/1', 'is_list/1',
|
|
515
|
+
'functor/3', 'arg/3', '=../2', 'copy_term/2', 'succ/2', 'plus/3',
|
|
516
|
+
'char_code/2', 'atom_codes/2', 'atom_chars/2', 'atom_length/2', 'atom_concat/3',
|
|
517
|
+
'sub_atom/5', 'number_codes/2', 'number_chars/2', 'atom_number/2',
|
|
518
|
+
'sort/2', 'msort/2', 'keysort/2', 'between/3', 'length/2',
|
|
519
|
+
'member/2', 'memberchk/2', 'append/3', 'append/2', 'reverse/2', 'nth0/3', 'nth1/3',
|
|
520
|
+
'last/2', 'select/3', 'sum_list/2', 'max_list/2', 'min_list/2', 'list_to_set/2',
|
|
521
|
+
'once/1', 'findall/3', 'forall/2', '\\+/1',
|
|
522
|
+
]);
|
|
523
|
+
|
|
524
|
+
// Settle a ground goal's first proof while its environment still holds every
|
|
525
|
+
// binding inside it, and keep it for reuse when replaying it has no effects:
|
|
526
|
+
// a proof that writes a stream or updates the database must perform those
|
|
527
|
+
// effects again, so it is not recorded.
|
|
528
|
+
function rememberGroundProof(key, proved) {
|
|
529
|
+
const node = settleProofNode(proved.node, proved.env);
|
|
530
|
+
if (replayIsPure(node)) groundProofs.set(key, node);
|
|
531
|
+
return { env: proved.env, node };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// The answer is cached on the node, so a deep derivation whose every level is
|
|
535
|
+
// remembered asks it once per level rather than once per level per descendant.
|
|
536
|
+
function replayIsPure(root) {
|
|
537
|
+
if (root.pureReplay !== undefined) return root.pureReplay;
|
|
538
|
+
const stack = [root];
|
|
539
|
+
let pure = true;
|
|
540
|
+
while (stack.length > 0) {
|
|
541
|
+
const node = stack.pop();
|
|
542
|
+
if (node !== root && node.pureReplay !== undefined) {
|
|
543
|
+
if (node.pureReplay) continue;
|
|
544
|
+
pure = false;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
const method = node.method;
|
|
548
|
+
if (method && typeof method === 'object' && (method.type === 'builtin' || method.type === 'library') &&
|
|
549
|
+
!PURE_REPLAY_STEPS.has(`${method.name}/${method.arity}`)) {
|
|
550
|
+
pure = false;
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
for (const child of node.children ?? []) stack.push(child);
|
|
554
|
+
}
|
|
555
|
+
root.pureReplay = pure;
|
|
556
|
+
return pure;
|
|
557
|
+
}
|
|
558
|
+
|
|
475
559
|
function goalMethod(type, goal) {
|
|
476
560
|
return {
|
|
477
561
|
type,
|
|
@@ -946,12 +1030,18 @@ export function clauseNumbering(program) {
|
|
|
946
1030
|
offset += span;
|
|
947
1031
|
}
|
|
948
1032
|
|
|
1033
|
+
// Keyed by file, then by clause position: one string key per clause cost
|
|
1034
|
+
// more than the numbering itself on programs with hundreds of thousands of
|
|
1035
|
+
// clauses.
|
|
949
1036
|
const bySource = new Map();
|
|
950
1037
|
const byNumber = new Map();
|
|
951
1038
|
for (const clause of program.clauses ?? []) {
|
|
952
1039
|
if (!isProgramClause(clause) || isUnnumberedDeclaration(clause)) continue;
|
|
953
|
-
const
|
|
954
|
-
|
|
1040
|
+
const { filename, clause: position } = clause.source;
|
|
1041
|
+
const number = (offsets.get(filename) ?? 0) + position;
|
|
1042
|
+
let numbers = bySource.get(filename);
|
|
1043
|
+
if (!numbers) bySource.set(filename, numbers = new Map());
|
|
1044
|
+
numbers.set(position, number);
|
|
955
1045
|
byNumber.set(number, { head: clause.head, body: clause.body ?? [] });
|
|
956
1046
|
}
|
|
957
1047
|
return { bySource, byNumber };
|
|
@@ -971,7 +1061,7 @@ export function isLibraryClause(clause) {
|
|
|
971
1061
|
}
|
|
972
1062
|
|
|
973
1063
|
function clauseNumberFor(numbering, method) {
|
|
974
|
-
return numbering.bySource.get(
|
|
1064
|
+
return numbering.bySource.get(method.filename)?.get(method.clause) ?? null;
|
|
975
1065
|
}
|
|
976
1066
|
|
|
977
1067
|
// The conclusion a node contributes, which is how a step names what it used.
|
|
@@ -1125,6 +1215,10 @@ function settleProofNode(root, env) {
|
|
|
1125
1215
|
const stack = [root];
|
|
1126
1216
|
while (stack.length) {
|
|
1127
1217
|
const node = stack.pop();
|
|
1218
|
+
// A settled node was settled with its whole subtree; a remembered ground
|
|
1219
|
+
// proof reached again from a deeper level need not be walked again.
|
|
1220
|
+
if (node.settled) continue;
|
|
1221
|
+
node.settled = true;
|
|
1128
1222
|
if (node.raw) {
|
|
1129
1223
|
node.goal = resolve(node.raw);
|
|
1130
1224
|
if (node.method?.type === 'builtin' || node.method?.type === 'library') node.sourceHead = node.goal;
|
|
@@ -1210,6 +1304,7 @@ function settlingResolver(env) {
|
|
|
1210
1304
|
export function proofNodeFor(program, goal, options = {}) {
|
|
1211
1305
|
liveSolver = options.solver ?? null;
|
|
1212
1306
|
chainBeyondDepth = options.maxDepth == null;
|
|
1307
|
+
groundProofs = options.groundProofs ?? null;
|
|
1213
1308
|
const maxDepth = options.maxDepth ?? 256;
|
|
1214
1309
|
const registry = options.registry ?? getEyePrologRegistry();
|
|
1215
1310
|
const env = options.env ?? new Env();
|
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,13 +318,20 @@ 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 ?? '');
|
|
317
329
|
this.filename = options.filename ?? '<input>';
|
|
318
330
|
this.pos = 0;
|
|
319
331
|
this.line = 1;
|
|
320
|
-
|
|
332
|
+
// Anonymous variables are numbered through a whole text; a caller parsing
|
|
333
|
+
// one text in pieces continues the count.
|
|
334
|
+
this.anonymous = options.anonymousStart ?? 0;
|
|
321
335
|
this.variables = new Map();
|
|
322
336
|
this.sourceMetadata = options.sourceMetadata !== false;
|
|
323
337
|
this.onWarning = typeof options.onWarning === 'function' ? options.onWarning : null;
|
|
@@ -346,8 +360,10 @@ class Parser {
|
|
|
346
360
|
this.token = this.nextToken();
|
|
347
361
|
}
|
|
348
362
|
convertCharacter(character) {
|
|
349
|
-
|
|
350
|
-
|
|
363
|
+
const state = this.parserFlagState;
|
|
364
|
+
// Almost no text installs a conversion; skip the map lookup per character.
|
|
365
|
+
if (state.charConversions.size === 0 || state.charConversion !== 'on' || !character) return character;
|
|
366
|
+
return state.charConversions.get(character) ?? character;
|
|
351
367
|
}
|
|
352
368
|
rawPeek(offset = 0) {
|
|
353
369
|
// The processor character set is an implementation-defined processor
|
|
@@ -364,6 +380,7 @@ class Parser {
|
|
|
364
380
|
return ch;
|
|
365
381
|
}
|
|
366
382
|
convertedSlice(start, end) {
|
|
383
|
+
if (this.parserFlagState.charConversions.size === 0) return this.source.slice(start, end);
|
|
367
384
|
let out = '';
|
|
368
385
|
for (const ch of this.source.slice(start, end)) out += this.convertCharacter(ch);
|
|
369
386
|
return out;
|
|
@@ -610,13 +627,10 @@ class Parser {
|
|
|
610
627
|
return { type: TOK.ATOM, text: ';', line };
|
|
611
628
|
}
|
|
612
629
|
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
'{': TOK.LBRACE, '}': TOK.RBRACE, ',': TOK.COMMA, '|': TOK.BAR, '.': TOK.DOT,
|
|
616
|
-
};
|
|
617
|
-
if (punct[ch]) {
|
|
630
|
+
const punctuation = PUNCTUATION_TOKENS.get(ch);
|
|
631
|
+
if (punctuation) {
|
|
618
632
|
this.take();
|
|
619
|
-
return { type:
|
|
633
|
+
return { type: punctuation, text: ch, line, precededByLayout };
|
|
620
634
|
}
|
|
621
635
|
if (ch === ':' && this.peek(1) === '-' &&
|
|
622
636
|
!(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
|
|
@@ -667,7 +681,9 @@ class Parser {
|
|
|
667
681
|
// A signed numeric literal is only recognized where a term may start.
|
|
668
682
|
// Otherwise the minus is the standard infix operator, so compact ISO
|
|
669
683
|
// syntax such as `X-1` must not be read as `X` followed by `-1`.
|
|
670
|
-
|
|
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 && (
|
|
671
687
|
[TOK.VAR, TOK.NUMBER, TOK.STRING, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE].includes(this.previousToken.type) ||
|
|
672
688
|
(this.previousToken.type === TOK.ATOM &&
|
|
673
689
|
(this.postfixOperators.has(this.previousToken.text) ||
|
|
@@ -675,7 +691,7 @@ class Parser {
|
|
|
675
691
|
!this.prefixOperators.has(this.previousToken.text))))
|
|
676
692
|
);
|
|
677
693
|
if (isDigitCode(ch.charCodeAt(0)) ||
|
|
678
|
-
(ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm)) {
|
|
694
|
+
(ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm())) {
|
|
679
695
|
const start = this.pos;
|
|
680
696
|
const negative = this.peek() === '-';
|
|
681
697
|
if (negative) this.take();
|
|
@@ -1431,7 +1447,10 @@ export function parseClauses(source, options = {}) {
|
|
|
1431
1447
|
const parserOptions = ownsParserFlagState
|
|
1432
1448
|
? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes, charConversion: 'on', charConversions: new Map() } }
|
|
1433
1449
|
: options;
|
|
1434
|
-
|
|
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) {
|
|
1435
1454
|
const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
|
|
1436
1455
|
if (clauses) return clauses;
|
|
1437
1456
|
if (ownsParserFlagState) {
|
|
@@ -1482,6 +1501,18 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1482
1501
|
let anonymous = 0;
|
|
1483
1502
|
let chunk = '';
|
|
1484
1503
|
let chunkStartLine = 1;
|
|
1504
|
+
let operatorState = null;
|
|
1505
|
+
// With source metadata, this path numbers clauses exactly as the general
|
|
1506
|
+
// parser does: one number per clause and per counted directive, none for a
|
|
1507
|
+
// query or a quad. Compact binary clauses carry no source, so they are not
|
|
1508
|
+
// used then.
|
|
1509
|
+
const trackSource = options.sourceMetadata === true;
|
|
1510
|
+
const sourceFilename = options.filename ?? '<input>';
|
|
1511
|
+
let clauseNumber = 0;
|
|
1512
|
+
const numbered = (clause, line) => {
|
|
1513
|
+
if (trackSource) clause.source = { filename: sourceFilename, line, clause: ++clauseNumber };
|
|
1514
|
+
return clause;
|
|
1515
|
+
};
|
|
1485
1516
|
|
|
1486
1517
|
const cached = (cache, key, create) => {
|
|
1487
1518
|
const existing = cache.get(key);
|
|
@@ -1491,22 +1522,19 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1491
1522
|
return value;
|
|
1492
1523
|
};
|
|
1493
1524
|
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
//
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
// zero, or the "-0" from issue #114, must collapse the same way the
|
|
1502
|
-
// scanner's own adjacent-minus BigInt construction already does), and a
|
|
1503
|
-
// float whose value is exactly zero must never print with a stray minus --
|
|
1504
|
-
// this implementation deliberately never produces -0.0 (term.js
|
|
1505
|
-
// numberTextFromDouble) -- without reformatting any non-zero float.
|
|
1525
|
+
// A number is a value, not a spelling (ISO 7.1.3), so the fast path writes
|
|
1526
|
+
// numbers exactly as the general parser does: an integer in canonical
|
|
1527
|
+
// decimal (a leading zero or "-0" collapses, issue #114), and a float as the
|
|
1528
|
+
// shortest text that reads back to the same double (term.js
|
|
1529
|
+
// numberTextFromDouble), so 0.10 is 0.1 and 6.02e23 is 6.02e+23 whichever
|
|
1530
|
+
// parser read the literal. A run therefore prints the same numbers with and
|
|
1531
|
+
// without --proof.
|
|
1506
1532
|
const canonicalFastNumberText = (text) => {
|
|
1507
|
-
if (text.includes('.')
|
|
1508
|
-
|
|
1533
|
+
if (text.includes('.')) {
|
|
1534
|
+
const value = Number(text);
|
|
1535
|
+
return Number.isFinite(value) ? numberTextFromDouble(value) : text;
|
|
1509
1536
|
}
|
|
1537
|
+
if (text.includes('e') || text.includes('E')) return text;
|
|
1510
1538
|
const first = text.charCodeAt(0);
|
|
1511
1539
|
if (first !== 45 && (text.length === 1 || first !== 48)) return text;
|
|
1512
1540
|
return BigInt(text).toString();
|
|
@@ -1826,15 +1854,46 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1826
1854
|
const simple = preparationConversionActive() ? null : parseSimple(text);
|
|
1827
1855
|
if (simple) {
|
|
1828
1856
|
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', sourceLine);
|
|
1829
|
-
accept(simple);
|
|
1857
|
+
accept(numbered(simple, sourceLine));
|
|
1830
1858
|
return true;
|
|
1831
1859
|
}
|
|
1832
1860
|
try {
|
|
1833
1861
|
const nestedWarning = typeof options.onWarning === 'function'
|
|
1834
1862
|
? (warning) => options.onWarning({ ...warning, line: warning.line + sourceLine - 1 })
|
|
1835
1863
|
: null;
|
|
1836
|
-
|
|
1837
|
-
|
|
1864
|
+
// One operator table serves every clause of this source: building it
|
|
1865
|
+
// per clause dominated the parse of large generated programs, and an
|
|
1866
|
+
// op/3 directive in one clause must govern the clauses after it anyway.
|
|
1867
|
+
operatorState ??= options.operatorState ?? createParserOperatorState(
|
|
1868
|
+
options.operatorDefinitions ?? [],
|
|
1869
|
+
options.includeDefaultOperators !== false,
|
|
1870
|
+
{ isoStrict: options.isoStrict === true },
|
|
1871
|
+
);
|
|
1872
|
+
const parser = new Parser(text, {
|
|
1873
|
+
...options,
|
|
1874
|
+
operatorState,
|
|
1875
|
+
sourceMetadata: trackSource,
|
|
1876
|
+
filename: sourceFilename,
|
|
1877
|
+
onWarning: nestedWarning,
|
|
1878
|
+
anonymousStart: trackSource ? anonymous : 0,
|
|
1879
|
+
});
|
|
1880
|
+
const parsed = parser.parseProgram();
|
|
1881
|
+
if (trackSource) anonymous = parser.anonymous;
|
|
1882
|
+
// The nested parser numbers lines and clauses from the start of this
|
|
1883
|
+
// chunk; place them in the whole source.
|
|
1884
|
+
let counted = 0;
|
|
1885
|
+
for (const clause of parsed) {
|
|
1886
|
+
if (trackSource && clause.source) {
|
|
1887
|
+
const relative = clause.source.clause;
|
|
1888
|
+
clause.source = { ...clause.source, line: clause.source.line + sourceLine - 1 };
|
|
1889
|
+
if (relative != null) {
|
|
1890
|
+
clause.source.clause = relative + clauseNumber;
|
|
1891
|
+
if (relative > counted) counted = relative;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
accept(clause);
|
|
1895
|
+
}
|
|
1896
|
+
clauseNumber += counted;
|
|
1838
1897
|
return true;
|
|
1839
1898
|
} catch (_) {
|
|
1840
1899
|
return false;
|
|
@@ -1853,13 +1912,13 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1853
1912
|
contentEnd = trimRangeEnd(source, contentStart, contentEnd);
|
|
1854
1913
|
if (contentStart < contentEnd && source.charCodeAt(contentStart) !== 37) {
|
|
1855
1914
|
if (!chunk && source.charCodeAt(contentEnd - 1) === 46 && !preparationConversionActive()) {
|
|
1856
|
-
if (emitFastBinaryRange(source, contentStart, contentEnd, lineNumber)) {
|
|
1915
|
+
if (!trackSource && emitFastBinaryRange(source, contentStart, contentEnd, lineNumber)) {
|
|
1857
1916
|
// The program builder accepted a compact binary clause directly.
|
|
1858
1917
|
} else {
|
|
1859
1918
|
const simple = parseFastRange(source, contentStart, contentEnd);
|
|
1860
1919
|
if (simple) {
|
|
1861
1920
|
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', lineNumber);
|
|
1862
|
-
accept(simple);
|
|
1921
|
+
accept(numbered(simple, lineNumber));
|
|
1863
1922
|
} else {
|
|
1864
1923
|
chunkStartLine = lineNumber;
|
|
1865
1924
|
chunk = source.slice(lineStart, lineEnd) + '\n';
|
package/src/program.js
CHANGED
|
@@ -958,6 +958,19 @@ function buildProgramFromSources(sources, options) {
|
|
|
958
958
|
const builder = new ProgramBuilder(options);
|
|
959
959
|
if (loadSourcesIntoBuilder(builder, sources, options, 'hybrid')) return builder.finish();
|
|
960
960
|
}
|
|
961
|
+
// With source metadata the compact parser still applies; it numbers clauses
|
|
962
|
+
// as the general parser does, and falls back to it for anything it cannot
|
|
963
|
+
// read. Strict ISO keeps the general parser throughout, and so does a text
|
|
964
|
+
// with a `?-` outside a comment: a quad's indented answer lines are layout
|
|
965
|
+
// the line-oriented compact parser does not read.
|
|
966
|
+
const hasQueries = sources.some((source) => {
|
|
967
|
+
const text = typeof source === 'string' ? source : source?.text ?? source?.source ?? '';
|
|
968
|
+
return /^[^%\n]*\?-/m.test(text);
|
|
969
|
+
});
|
|
970
|
+
if (options.sourceMetadata !== false && !hasModuleDirectives && !hasQueries && options.isoStrict !== true) {
|
|
971
|
+
const builder = new ProgramBuilder(options);
|
|
972
|
+
if (loadSourcesIntoBuilder(builder, sources, options, true)) return builder.finish();
|
|
973
|
+
}
|
|
961
974
|
|
|
962
975
|
const builder = new ProgramBuilder(options);
|
|
963
976
|
loadSourcesIntoBuilder(builder, sources, options, false);
|
package/src/proof-document.js
CHANGED
|
@@ -36,8 +36,9 @@ export function checkedProofBlocks(program, claimed, { registry, proofDetail = '
|
|
|
36
36
|
const roots = [];
|
|
37
37
|
const unexplained = [];
|
|
38
38
|
const questions = goals.filter((goal) => typeof goal !== 'string');
|
|
39
|
+
const groundProofs = new Map();
|
|
39
40
|
for (const fact of claimed) {
|
|
40
|
-
const node = proofNodeFor(program, fact, { registry, proofDetail, solver, questions });
|
|
41
|
+
const node = proofNodeFor(program, fact, { registry, proofDetail, solver, questions, groundProofs });
|
|
41
42
|
if (node) roots.push(node);
|
|
42
43
|
else unexplained.push(fact);
|
|
43
44
|
}
|
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/src/write.js
CHANGED
|
@@ -211,7 +211,18 @@ function operatorName(name) {
|
|
|
211
211
|
return quoteAtom(name);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Writers pass the same definitions array for every term of a run, so build
|
|
215
|
+
// its lookup table once instead of once per written term.
|
|
216
|
+
const operatorTables = new WeakMap();
|
|
217
|
+
|
|
214
218
|
function operatorTable(definitions) {
|
|
219
|
+
if (!definitions) return buildOperatorTable(definitions);
|
|
220
|
+
let table = operatorTables.get(definitions);
|
|
221
|
+
if (!table) operatorTables.set(definitions, table = buildOperatorTable(definitions));
|
|
222
|
+
return table;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildOperatorTable(definitions) {
|
|
215
226
|
const table = new Map();
|
|
216
227
|
for (const definition of definitions ?? []) {
|
|
217
228
|
if (!definition || definition.priority === 0) continue;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
%
|
|
1
|
+
% Signed, decimal, and exponent numeric literals read back as their values (ISO 7.1.3, 7.10.5 c).
|
|
2
2
|
raw(negative_decimal, -3.5).
|
|
3
3
|
raw(positive_exp, 6.02e23).
|
|
4
4
|
raw(negative_exp, -1.0e-3).
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
answer(negative_decimal, -3.5).
|
|
2
|
-
answer(positive_exp, 6.
|
|
3
|
-
answer(negative_exp, -
|
|
2
|
+
answer(positive_exp, 6.02e+23).
|
|
3
|
+
answer(negative_exp, -0.001).
|
|
@@ -5,7 +5,7 @@ value(empty_atom, '').
|
|
|
5
5
|
value(char_list, "line\nquote: \"ok\"").
|
|
6
6
|
value(integer, -42).
|
|
7
7
|
value(decimal, 0.25).
|
|
8
|
-
value(scientific,
|
|
8
|
+
value(scientific, 0.00125).
|
|
9
9
|
value(compound, pair(3, nested(atom, "xy"))).
|
|
10
10
|
value(arity_zero_atom, nil).
|
|
11
11
|
value(empty_list, []).
|
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) {
|
|
@@ -6013,21 +6013,16 @@ answer(Result) :- countdown(2048, Result), Result = 2048.
|
|
|
6013
6013
|
const stdout = run(program, { goal }).stdout;
|
|
6014
6014
|
assertEqual(stdout.split('\n')[0], expected, `${program.trim()}: ${goal}`);
|
|
6015
6015
|
}
|
|
6016
|
-
//
|
|
6017
|
-
//
|
|
6018
|
-
//
|
|
6019
|
-
//
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
run(
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
assertEqual(
|
|
6027
|
-
run('raw(a, -1.0e-3).\n', { goal: 'raw(_, B), write_canonical(B), nl' }).stdout.split('\n')[0],
|
|
6028
|
-
'-1.0e-3',
|
|
6029
|
-
'non-zero negative exponent notation is preserved, not reformatted to -0.001',
|
|
6030
|
-
);
|
|
6016
|
+
// A float is a value, not a spelling (ISO 7.1.3, 7.10.5 c): the fast
|
|
6017
|
+
// loader writes it as the general parser does, so a run prints the same
|
|
6018
|
+
// number with and without --proof. A three-argument fact is outside the
|
|
6019
|
+
// compact shape and so is read by the general parser.
|
|
6020
|
+
for (const [literal, expected] of [['6.02e23', '6.02e+23'], ['-1.0e-3', '-0.001'], ['0.10', '0.1']]) {
|
|
6021
|
+
const fast = run(`raw(a, ${literal}).\n`, { goal: 'raw(_, B), write_canonical(B), nl' }).stdout.split('\n')[0];
|
|
6022
|
+
const general = run(`raw(a, b, ${literal}).\n`, { goal: 'raw(_, _, B), write_canonical(B), nl' }).stdout.split('\n')[0];
|
|
6023
|
+
assertEqual(fast, expected, `${literal} through the fast loader`);
|
|
6024
|
+
assertEqual(general, expected, `${literal} through the general parser`);
|
|
6025
|
+
}
|
|
6031
6026
|
},
|
|
6032
6027
|
},
|
|
6033
6028
|
{
|
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;
|