eyeprolog 1.4.7 → 1.4.10
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/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/cleanup.js +20 -4
- package/src/cli.js +17 -1
- package/src/io.js +6 -0
- package/src/iso.js +84 -16
- package/src/lib/iso_ext.pl +1 -2
- package/src/lib/random.pl +9 -4
- package/src/parser.js +79 -6
- package/src/program-indexing.js +102 -12
- package/src/program.js +71 -13
- package/src/scryer-compat.js +41 -1
- package/src/solver.js +201 -29
- package/src/standard-library.js +2 -1
- package/src/term.js +74 -18
- package/test/run-examples.mjs +5 -0
- package/test/run-regression.mjs +58 -62
- package/the-art-of-eyeprolog.md +11 -4
package/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface EyePrologRunOptions {
|
|
|
27
27
|
isoStrict?: boolean;
|
|
28
28
|
/** Autoload uniquely mapped predicates from the conservative interop profile. Defaults to true outside strict ISO mode. */
|
|
29
29
|
autoload?: boolean;
|
|
30
|
+
/** Automatically table recursive user predicates in normal mode. Defaults to true; set false for traditional depth-first control. */
|
|
31
|
+
autoTabling?: boolean;
|
|
30
32
|
/** Initial ISO interpretation of double-quoted list notation. Defaults to chars. */
|
|
31
33
|
doubleQuotes?: 'chars' | 'codes' | 'atom';
|
|
32
34
|
ioOptions?: {
|
package/package.json
CHANGED
package/src/cleanup.js
CHANGED
|
@@ -46,7 +46,7 @@ export function installCleanupLifecycle(Solver) {
|
|
|
46
46
|
if (!completed) {
|
|
47
47
|
let closeError = null;
|
|
48
48
|
try {
|
|
49
|
-
closeOwnedSearchStacks(this, state, owner, thrown != null);
|
|
49
|
+
if (state.active) closeOwnedSearchStacks(this, state, owner, thrown != null);
|
|
50
50
|
} catch (error) {
|
|
51
51
|
closeError = error;
|
|
52
52
|
}
|
|
@@ -71,6 +71,7 @@ function ensureLifecycleState(solver) {
|
|
|
71
71
|
const state = {
|
|
72
72
|
owner: null,
|
|
73
73
|
stackOwners: new WeakMap(),
|
|
74
|
+
active: false,
|
|
74
75
|
};
|
|
75
76
|
Object.defineProperty(solver, cleanupLifecycleState, { value: state });
|
|
76
77
|
|
|
@@ -81,7 +82,7 @@ function ensureLifecycleState(solver) {
|
|
|
81
82
|
solveStacks.push = function cleanupAwarePush(...stacks) {
|
|
82
83
|
for (const stack of stacks) {
|
|
83
84
|
state.stackOwners.set(stack, state.owner);
|
|
84
|
-
patchSearchStack(stack);
|
|
85
|
+
if (state.active) patchSearchStack(stack);
|
|
85
86
|
}
|
|
86
87
|
return originalPush.apply(this, stacks);
|
|
87
88
|
};
|
|
@@ -92,8 +93,10 @@ function ensureLifecycleState(solver) {
|
|
|
92
93
|
// unwinding an exception, a cleanup exception must not replace it. Normal
|
|
93
94
|
// consumer pruning closes frames earlier in cleanupAwareSolve(), where
|
|
94
95
|
// cleanup errors are allowed to propagate.
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
if (state.active) {
|
|
97
|
+
for (let index = removed.length - 1; index >= 0; index--) {
|
|
98
|
+
closeSearchStack(removed[index], true);
|
|
99
|
+
}
|
|
97
100
|
}
|
|
98
101
|
return removed;
|
|
99
102
|
};
|
|
@@ -111,6 +114,17 @@ function withOwner(state, owner, operation) {
|
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
|
|
117
|
+
function activateCleanupLifecycle(solver) {
|
|
118
|
+
const state = ensureLifecycleState(solver);
|
|
119
|
+
if (state.active) return state;
|
|
120
|
+
state.active = true;
|
|
121
|
+
for (const stack of solver.solveStacks) {
|
|
122
|
+
if (!state.stackOwners.has(stack)) state.stackOwners.set(stack, state.owner);
|
|
123
|
+
patchSearchStack(stack);
|
|
124
|
+
}
|
|
125
|
+
return state;
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
function patchSearchStack(stack) {
|
|
115
129
|
if (stack[searchStackPatched]) return;
|
|
116
130
|
Object.defineProperty(stack, searchStackPatched, { value: true });
|
|
@@ -189,11 +203,13 @@ function firstSetupSolution(solver, setup, env) {
|
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
function callCleanupBuiltin({ solver, goal, env }) {
|
|
206
|
+
activateCleanupLifecycle(solver);
|
|
192
207
|
const cleanup = callableTerm(goal.args[1], env);
|
|
193
208
|
return cleanupProtectedIterator(solver, goal.args[0], cleanup, env);
|
|
194
209
|
}
|
|
195
210
|
|
|
196
211
|
function setupCallCleanupBuiltin({ solver, goal, env }) {
|
|
212
|
+
activateCleanupLifecycle(solver);
|
|
197
213
|
let protectedIterator = null;
|
|
198
214
|
let pending = true;
|
|
199
215
|
const iterator = (function* setupThenProtected() {
|
package/src/cli.js
CHANGED
|
@@ -30,6 +30,7 @@ export async function main(argv) {
|
|
|
30
30
|
isoStrict: false,
|
|
31
31
|
portable: false,
|
|
32
32
|
autoload: true,
|
|
33
|
+
autoTabling: true,
|
|
33
34
|
version: false,
|
|
34
35
|
warnings: false,
|
|
35
36
|
goals: [],
|
|
@@ -57,6 +58,8 @@ export async function main(argv) {
|
|
|
57
58
|
options.portable = true;
|
|
58
59
|
} else if (!endOptions && arg === '--no-autoload') {
|
|
59
60
|
options.autoload = false;
|
|
61
|
+
} else if (!endOptions && arg === '--no-auto-table') {
|
|
62
|
+
options.autoTabling = false;
|
|
60
63
|
} else if (!endOptions && (arg === '--version' || arg === '-v')) {
|
|
61
64
|
options.version = true;
|
|
62
65
|
} else if (!endOptions && (arg === '--warnings' || arg === '-w')) {
|
|
@@ -153,7 +156,9 @@ export async function main(argv) {
|
|
|
153
156
|
sourceMetadata: options.proof || options.isoStrict,
|
|
154
157
|
isoStrict: options.isoStrict,
|
|
155
158
|
autoload: options.autoload,
|
|
159
|
+
autoTabling: options.autoTabling,
|
|
156
160
|
autoloadGoals: options.goals,
|
|
161
|
+
onWarning: printSourceWarning,
|
|
157
162
|
});
|
|
158
163
|
|
|
159
164
|
const portabilityFailures = program.interopPortabilityWarnings ?? [];
|
|
@@ -204,7 +209,10 @@ async function runDefault(engine, program, options) {
|
|
|
204
209
|
const solver = new engine.Solver(program, {
|
|
205
210
|
registry,
|
|
206
211
|
isoStrict: options.isoStrict,
|
|
207
|
-
ioOptions: {
|
|
212
|
+
ioOptions: {
|
|
213
|
+
write: (text) => process.stdout.write(String(text)),
|
|
214
|
+
errorWrite: (text) => process.stderr.write(String(text)),
|
|
215
|
+
},
|
|
208
216
|
});
|
|
209
217
|
program = solver.program;
|
|
210
218
|
const goals = engine.normalizeGoals(options.goals, solver);
|
|
@@ -251,6 +259,7 @@ Options:
|
|
|
251
259
|
reject EyeProlog language extensions and disable automatic tabling.
|
|
252
260
|
--portable Enforce the EyeProlog/Trealla/Scryer interop profile.
|
|
253
261
|
--no-autoload Disable conservative interop predicate autoloading.
|
|
262
|
+
--no-auto-table Use traditional depth-first control instead of automatic recursive tabling.
|
|
254
263
|
-v, --version Show the package version and exit.
|
|
255
264
|
-w, --warnings Print non-fatal portability warnings to stderr.
|
|
256
265
|
-g, --goal goal Solve goal and print its ground answers; may be repeated.
|
|
@@ -271,6 +280,13 @@ function readStdin() {
|
|
|
271
280
|
});
|
|
272
281
|
}
|
|
273
282
|
|
|
283
|
+
|
|
284
|
+
function printSourceWarning(warning) {
|
|
285
|
+
if (warning.kind !== 'singleton') return;
|
|
286
|
+
process.stderr.write(`Warning: singleton: ${warning.name}, near ${warning.filename}:${warning.line}
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
|
|
274
290
|
function printWarnings(program) {
|
|
275
291
|
for (const warning of program.interopPortabilityWarnings ?? []) {
|
|
276
292
|
if (warning.kind === 'library') {
|
package/src/io.js
CHANGED
|
@@ -64,6 +64,12 @@ export class StreamManager {
|
|
|
64
64
|
this.add({ id: 1, alias: 'user_output', mode: 'append', type: 'text',
|
|
65
65
|
content: '', position: 0, path: '', reposition: false,
|
|
66
66
|
eofAction: 'reset', standard: true, write: this.output, pastEnd: false });
|
|
67
|
+
// user_error is a conventional de-facto standard stream used by the
|
|
68
|
+
// portable Scryer/Trealla libraries. Keep it on a negative internal handle
|
|
69
|
+
// so the first user-opened ISO stream retains its historical id 2.
|
|
70
|
+
this.add({ id: -1, alias: 'user_error', mode: 'append', type: 'text',
|
|
71
|
+
content: '', position: 0, path: '', reposition: false,
|
|
72
|
+
eofAction: 'reset', standard: true, write: options.errorWrite ?? this.output, pastEnd: false });
|
|
67
73
|
this.currentInput = 0;
|
|
68
74
|
this.currentOutput = 1;
|
|
69
75
|
}
|
package/src/iso.js
CHANGED
|
@@ -581,19 +581,85 @@ function isPartialList(list, env) {
|
|
|
581
581
|
return cursor.type === VAR;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
const freshCopyGroundCache = new WeakMap();
|
|
585
|
+
|
|
586
|
+
function structurallyVariableFree(term) {
|
|
587
|
+
if (term?.type === VAR) return false;
|
|
588
|
+
if (term?.type !== COMPOUND) return true;
|
|
589
|
+
const known = freshCopyGroundCache.get(term);
|
|
590
|
+
if (known !== undefined) return known;
|
|
591
|
+
|
|
592
|
+
const pending = [{ term, visited: false }];
|
|
593
|
+
while (pending.length > 0) {
|
|
594
|
+
const frame = pending.pop();
|
|
595
|
+
const current = frame.term;
|
|
596
|
+
if (current?.type !== COMPOUND || freshCopyGroundCache.has(current)) continue;
|
|
597
|
+
if (!frame.visited) {
|
|
598
|
+
pending.push({ term: current, visited: true });
|
|
599
|
+
for (let index = current.args.length - 1; index >= 0; index--) {
|
|
600
|
+
const child = current.args[index];
|
|
601
|
+
if (child?.type === COMPOUND && !freshCopyGroundCache.has(child)) {
|
|
602
|
+
pending.push({ term: child, visited: false });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
let ground = true;
|
|
608
|
+
for (const child of current.args) {
|
|
609
|
+
if (child?.type === VAR || (child?.type === COMPOUND && freshCopyGroundCache.get(child) !== true)) {
|
|
610
|
+
ground = false;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
freshCopyGroundCache.set(current, ground);
|
|
615
|
+
}
|
|
616
|
+
return freshCopyGroundCache.get(term) === true;
|
|
617
|
+
}
|
|
618
|
+
|
|
584
619
|
function freshCopy(term, env, variables = new Map(), id = ++isoFresh) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
620
|
+
const copyNode = (source) => {
|
|
621
|
+
if (source.type === VAR) {
|
|
622
|
+
if (!variables.has(source.name)) variables.set(source.name, variable(`__copy${id}_${variables.size}`));
|
|
623
|
+
return variables.get(source.name);
|
|
624
|
+
}
|
|
625
|
+
if (source.type !== COMPOUND) return source;
|
|
626
|
+
// Ground terms are immutable in EyeProlog. Sharing them is observably
|
|
627
|
+
// equivalent to copying and avoids rebuilding large lists for assertz/1,
|
|
628
|
+
// copy_term/2, bagof/findall storage, and exception terms.
|
|
629
|
+
if (structurallyVariableFree(source)) return source;
|
|
630
|
+
const copied = compound(source.name, new Array(source.args.length));
|
|
631
|
+
// Module qualification is execution context, not logical variable state.
|
|
632
|
+
// Preserve it across copy_term/2 so meta-predicate closures keep the caller
|
|
633
|
+
// module they received before a library copies and later invokes them.
|
|
634
|
+
if (source.module != null) copied.module = source.module;
|
|
635
|
+
return copied;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const source = deref(term, env);
|
|
639
|
+
const root = copyNode(source);
|
|
640
|
+
if (source.type !== COMPOUND || source.args.length === 0) return root;
|
|
641
|
+
|
|
642
|
+
// copy_term/2 is routinely used on long lists and large machine states.
|
|
643
|
+
// Traverse iteratively so term depth is independent of the host call stack.
|
|
644
|
+
// The map also preserves rational-tree cycles when occurs_check is disabled.
|
|
645
|
+
const copies = new Map([[source, root]]);
|
|
646
|
+
const pending = [{ source, target: root }];
|
|
647
|
+
while (pending.length > 0) {
|
|
648
|
+
const current = pending.pop();
|
|
649
|
+
for (let index = 0; index < current.source.args.length; index++) {
|
|
650
|
+
const childSource = deref(current.source.args[index], env);
|
|
651
|
+
let child = childSource.type === COMPOUND ? copies.get(childSource) : null;
|
|
652
|
+
if (child == null) {
|
|
653
|
+
child = copyNode(childSource);
|
|
654
|
+
if (childSource.type === COMPOUND && child !== childSource) {
|
|
655
|
+
copies.set(childSource, child);
|
|
656
|
+
if (childSource.args.length > 0) pending.push({ source: childSource, target: child });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
current.target.args[index] = child;
|
|
660
|
+
}
|
|
589
661
|
}
|
|
590
|
-
|
|
591
|
-
const copied = compound(term.name, term.args.map((arg) => freshCopy(arg, env, variables, id)));
|
|
592
|
-
// Module qualification is execution context, not logical variable state.
|
|
593
|
-
// Preserve it across copy_term/2 so meta-predicate closures keep the caller
|
|
594
|
-
// module they received before a library copies and later invokes them.
|
|
595
|
-
if (term.module != null) copied.module = term.module;
|
|
596
|
-
return copied;
|
|
662
|
+
return root;
|
|
597
663
|
}
|
|
598
664
|
function* copyTermBuiltin({ goal, env }) {
|
|
599
665
|
const next = env.clone();
|
|
@@ -609,13 +675,15 @@ function* termVariablesBuiltin({ goal, env }) {
|
|
|
609
675
|
}
|
|
610
676
|
const found = [];
|
|
611
677
|
const seen = new Set();
|
|
612
|
-
const
|
|
613
|
-
|
|
678
|
+
const pending = [goal.args[0]];
|
|
679
|
+
while (pending.length > 0) {
|
|
680
|
+
const term = deref(pending.pop(), env);
|
|
614
681
|
if (term.type === VAR) {
|
|
615
682
|
if (!seen.has(term.name)) { seen.add(term.name); found.push(term); }
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
for (let index = term.args.length - 1; index >= 0; index--) pending.push(term.args[index]);
|
|
686
|
+
}
|
|
619
687
|
const next = env.clone();
|
|
620
688
|
if (unify(goal.args[1], listFromItems(found), next)) yield next;
|
|
621
689
|
}
|
package/src/lib/iso_ext.pl
CHANGED
|
@@ -28,8 +28,7 @@ call_nth(Goal, Nth) :- eyeprolog__call_nth(Goal, Nth).
|
|
|
28
28
|
countall(Goal, Count) :- eyeprolog__countall(Goal, Count).
|
|
29
29
|
|
|
30
30
|
forall(Condition, Action) :-
|
|
31
|
-
|
|
32
|
-
iso_ext__call_all(Actions).
|
|
31
|
+
\+ (Condition, \+ Action).
|
|
33
32
|
|
|
34
33
|
succ(X, S) :-
|
|
35
34
|
var(X), !,
|
package/src/lib/random.pl
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/** Reproducible pseudo-random values.
|
|
2
2
|
|
|
3
3
|
random/3 is EyeProlog's explicit-state interface. maybe/0,
|
|
4
|
-
random_integer/3, and set_random/1 are the
|
|
5
|
-
interface
|
|
6
|
-
|
|
4
|
+
random/1, random_integer/3, and set_random/1 are the mutable-seed library
|
|
5
|
+
interface used for compatibility. random/1 uses EyeProlog's private native
|
|
6
|
+
state step for hot-loop performance; random/3 remains the portable explicit-
|
|
7
|
+
state Park-Miller generator, and both retain the same sequence.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
:- module(random, [maybe/0, random/3, random_integer/3, set_random/1]).
|
|
10
|
+
:- module(random, [maybe/0, random/1, random/3, random_integer/3, set_random/1]).
|
|
10
11
|
|
|
11
12
|
:- use_module(library(debug), [bb_global_get/2, bb_put/2]).
|
|
12
13
|
:- use_module(library(error), [instantiation_error/1, type_error/3]).
|
|
@@ -14,6 +15,10 @@
|
|
|
14
15
|
maybe :-
|
|
15
16
|
random_integer(0, 2, 0).
|
|
16
17
|
|
|
18
|
+
|
|
19
|
+
random(Value) :-
|
|
20
|
+
eyeprolog__random_value(Value).
|
|
21
|
+
|
|
17
22
|
random_integer(Lower, Upper, R) :-
|
|
18
23
|
( var(Lower) -> instantiation_error(random_integer/3)
|
|
19
24
|
; var(Upper) -> instantiation_error(random_integer/3)
|
package/src/parser.js
CHANGED
|
@@ -246,6 +246,8 @@ class Parser {
|
|
|
246
246
|
this.anonymous = 0;
|
|
247
247
|
this.variables = new Map();
|
|
248
248
|
this.sourceMetadata = options.sourceMetadata !== false;
|
|
249
|
+
this.onWarning = typeof options.onWarning === 'function' ? options.onWarning : null;
|
|
250
|
+
this.currentSourceTermLine = 1;
|
|
249
251
|
this.strictIso = options.isoStrict === true;
|
|
250
252
|
this.parserFlagState = options.parserFlagState ?? {
|
|
251
253
|
doubleQuotes: options.doubleQuotes ?? 'chars',
|
|
@@ -1010,9 +1012,14 @@ class Parser {
|
|
|
1010
1012
|
parseProgram(emit = null) {
|
|
1011
1013
|
const clauses = emit ? null : [];
|
|
1012
1014
|
let clauseNumber = 0;
|
|
1013
|
-
const
|
|
1015
|
+
const rawAccept = emit ?? ((clause) => clauses.push(clause));
|
|
1016
|
+
const accept = (item) => {
|
|
1017
|
+
reportClauseSingletonWarnings(item, this.onWarning, this.filename, this.currentSourceTermLine);
|
|
1018
|
+
rawAccept(item);
|
|
1019
|
+
};
|
|
1014
1020
|
while (this.token.type !== TOK.EOF) {
|
|
1015
1021
|
const line = this.token.line;
|
|
1022
|
+
this.currentSourceTermLine = line;
|
|
1016
1023
|
// Prefix operator notation needs one program-level distinction so
|
|
1017
1024
|
// a comma in `?- A, B.` remains inside the query rather than outside the
|
|
1018
1025
|
// prefix term. Ordinary functional notation is parsed as a term and then
|
|
@@ -1226,6 +1233,29 @@ function listAtomNames(term) {
|
|
|
1226
1233
|
}
|
|
1227
1234
|
|
|
1228
1235
|
|
|
1236
|
+
function collectClauseVariableOccurrences(term, counts) {
|
|
1237
|
+
if (term == null) return;
|
|
1238
|
+
if (term.type === 'var') {
|
|
1239
|
+
counts.set(term.name, (counts.get(term.name) ?? 0) + 1);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (term.type === COMPOUND) {
|
|
1243
|
+
for (const arg of term.args) collectClauseVariableOccurrences(arg, counts);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function reportClauseSingletonWarnings(clause, onWarning, filename, line) {
|
|
1248
|
+
if (typeof onWarning !== 'function' || clause?.kind === 'quad') return;
|
|
1249
|
+
const counts = new Map();
|
|
1250
|
+
collectClauseVariableOccurrences(clause.head, counts);
|
|
1251
|
+
for (const goal of clause.body ?? []) collectClauseVariableOccurrences(goal, counts);
|
|
1252
|
+
for (const [name, count] of counts) {
|
|
1253
|
+
if (count !== 1 || String(name).startsWith('_')) continue;
|
|
1254
|
+
onWarning({ kind: 'singleton', name, filename: filename ?? '<input>', line });
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
|
|
1229
1259
|
export function parseClauses(source, options = {}) {
|
|
1230
1260
|
const ownsParserFlagState = options.parserFlagState == null;
|
|
1231
1261
|
const initialDoubleQuotes = options.doubleQuotes ?? 'chars';
|
|
@@ -1282,6 +1312,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1282
1312
|
const accept = emit ?? ((clause) => clauses.push(clause));
|
|
1283
1313
|
let anonymous = 0;
|
|
1284
1314
|
let chunk = '';
|
|
1315
|
+
let chunkStartLine = 1;
|
|
1285
1316
|
|
|
1286
1317
|
const cached = (cache, key, create) => {
|
|
1287
1318
|
const existing = cache.get(key);
|
|
@@ -1466,12 +1497,42 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1466
1497
|
return -1;
|
|
1467
1498
|
};
|
|
1468
1499
|
|
|
1469
|
-
const
|
|
1500
|
+
const reportFastBinarySingletonWarnings = (head, body, line) => {
|
|
1501
|
+
if (typeof options.onWarning !== 'function') return;
|
|
1502
|
+
const variables = [];
|
|
1503
|
+
const add = (type, name) => {
|
|
1504
|
+
if (type !== 'var' || String(name).startsWith('_')) return;
|
|
1505
|
+
variables.push(name);
|
|
1506
|
+
};
|
|
1507
|
+
add(head.arg0Type, head.arg0Name);
|
|
1508
|
+
add(head.arg1Type, head.arg1Name);
|
|
1509
|
+
if (body != null) {
|
|
1510
|
+
add(body.arg0Type, body.arg0Name);
|
|
1511
|
+
add(body.arg1Type, body.arg1Name);
|
|
1512
|
+
}
|
|
1513
|
+
for (let i = 0; i < variables.length; i++) {
|
|
1514
|
+
let count = 0;
|
|
1515
|
+
for (let j = 0; j < variables.length; j++) {
|
|
1516
|
+
if (variables[i] === variables[j]) count++;
|
|
1517
|
+
}
|
|
1518
|
+
if (count === 1) {
|
|
1519
|
+
options.onWarning({
|
|
1520
|
+
kind: 'singleton',
|
|
1521
|
+
name: variables[i],
|
|
1522
|
+
filename: options.filename ?? '<input>',
|
|
1523
|
+
line,
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
};
|
|
1528
|
+
|
|
1529
|
+
const emitFastBinaryRange = (text, start, end, line) => {
|
|
1470
1530
|
if (!emitBinary || start >= end || text.charCodeAt(end - 1) !== 46) return false;
|
|
1471
1531
|
const termEnd = end - 1;
|
|
1472
1532
|
const rule = findRuleInRange(text, start, termEnd);
|
|
1473
1533
|
if (rule < 0) {
|
|
1474
1534
|
if (!parseBinaryRawRange(text, start, termEnd, rawHead)) return false;
|
|
1535
|
+
reportFastBinarySingletonWarnings(rawHead, null, line);
|
|
1475
1536
|
emitBinary(rawHead.name,
|
|
1476
1537
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1477
1538
|
null, null, null, null, null);
|
|
@@ -1480,6 +1541,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1480
1541
|
if (findRuleInRange(text, rule + 2, termEnd) >= 0 ||
|
|
1481
1542
|
!parseBinaryRawRange(text, start, rule, rawHead) ||
|
|
1482
1543
|
!parseBinaryRawRange(text, rule + 2, termEnd, rawBody)) return false;
|
|
1544
|
+
reportFastBinarySingletonWarnings(rawHead, rawBody, line);
|
|
1483
1545
|
emitBinary(rawHead.name,
|
|
1484
1546
|
rawHead.arg0Type, rawHead.arg0Name, rawHead.arg1Type, rawHead.arg1Name,
|
|
1485
1547
|
rawBody.name, rawBody.arg0Type, rawBody.arg0Name, rawBody.arg1Type, rawBody.arg1Name);
|
|
@@ -1541,6 +1603,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1541
1603
|
|
|
1542
1604
|
const flush = () => {
|
|
1543
1605
|
const text = chunk.trim();
|
|
1606
|
+
const sourceLine = chunkStartLine;
|
|
1544
1607
|
chunk = '';
|
|
1545
1608
|
if (!text) return true;
|
|
1546
1609
|
// Once a preparation-time char_conversion/2 mapping is active, every
|
|
@@ -1549,11 +1612,15 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1549
1612
|
// here would silently bypass Convc for otherwise simple clauses.
|
|
1550
1613
|
const simple = preparationConversionActive() ? null : parseSimple(text);
|
|
1551
1614
|
if (simple) {
|
|
1615
|
+
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', sourceLine);
|
|
1552
1616
|
accept(simple);
|
|
1553
1617
|
return true;
|
|
1554
1618
|
}
|
|
1555
1619
|
try {
|
|
1556
|
-
const
|
|
1620
|
+
const nestedWarning = typeof options.onWarning === 'function'
|
|
1621
|
+
? (warning) => options.onWarning({ ...warning, line: warning.line + sourceLine - 1 })
|
|
1622
|
+
: null;
|
|
1623
|
+
const parsed = new Parser(text, { ...options, sourceMetadata: false, onWarning: nestedWarning }).parseProgram();
|
|
1557
1624
|
for (const clause of parsed) accept(clause);
|
|
1558
1625
|
return true;
|
|
1559
1626
|
} catch (_) {
|
|
@@ -1562,6 +1629,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1562
1629
|
};
|
|
1563
1630
|
|
|
1564
1631
|
let lineStart = 0;
|
|
1632
|
+
let lineNumber = 1;
|
|
1565
1633
|
while (lineStart <= source.length) {
|
|
1566
1634
|
let lineEnd = source.indexOf('\n', lineStart);
|
|
1567
1635
|
if (lineEnd < 0) lineEnd = source.length;
|
|
@@ -1571,17 +1639,21 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1571
1639
|
[contentStart, contentEnd] = trimRange(source, contentStart, contentEnd);
|
|
1572
1640
|
if (contentStart < contentEnd && source.charCodeAt(contentStart) !== 37) {
|
|
1573
1641
|
if (!chunk && source.charCodeAt(contentEnd - 1) === 46 && !preparationConversionActive()) {
|
|
1574
|
-
if (emitFastBinaryRange(source, contentStart, contentEnd)) {
|
|
1642
|
+
if (emitFastBinaryRange(source, contentStart, contentEnd, lineNumber)) {
|
|
1575
1643
|
// The program builder accepted a compact binary clause directly.
|
|
1576
1644
|
} else {
|
|
1577
1645
|
const simple = parseFastRange(source, contentStart, contentEnd);
|
|
1578
|
-
if (simple)
|
|
1579
|
-
|
|
1646
|
+
if (simple) {
|
|
1647
|
+
reportClauseSingletonWarnings(simple, options.onWarning, options.filename ?? '<input>', lineNumber);
|
|
1648
|
+
accept(simple);
|
|
1649
|
+
} else {
|
|
1650
|
+
chunkStartLine = lineNumber;
|
|
1580
1651
|
chunk = source.slice(lineStart, lineEnd) + '\n';
|
|
1581
1652
|
if (!flush()) return null;
|
|
1582
1653
|
}
|
|
1583
1654
|
}
|
|
1584
1655
|
} else {
|
|
1656
|
+
if (!chunk) chunkStartLine = lineNumber;
|
|
1585
1657
|
chunk += source.slice(lineStart, lineEnd) + '\n';
|
|
1586
1658
|
if (source.charCodeAt(contentEnd - 1) === 46) {
|
|
1587
1659
|
if (!flush()) return null;
|
|
@@ -1590,6 +1662,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
|
|
|
1590
1662
|
}
|
|
1591
1663
|
if (lineEnd === source.length) break;
|
|
1592
1664
|
lineStart = lineEnd + 1;
|
|
1665
|
+
lineNumber++;
|
|
1593
1666
|
}
|
|
1594
1667
|
if (chunk.trim() && !flush()) return null;
|
|
1595
1668
|
return clauses ?? true;
|
package/src/program-indexing.js
CHANGED
|
@@ -86,13 +86,24 @@ export function clauseHasCut(clause) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function termContainsCut(term) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
const pending = [term];
|
|
90
|
+
while (pending.length > 0) {
|
|
91
|
+
const current = pending.pop();
|
|
92
|
+
if (current?.type === ATOM && current.name === '!') return true;
|
|
93
|
+
if (current?.type !== COMPOUND) continue;
|
|
94
|
+
for (let index = current.args.length - 1; index >= 0; index--) pending.push(current.args[index]);
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export function termHasNoVariables(term) {
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
const pending = [term];
|
|
101
|
+
while (pending.length > 0) {
|
|
102
|
+
const current = pending.pop();
|
|
103
|
+
if (!current || current.type === 'var') return false;
|
|
104
|
+
for (let index = current.args?.length - 1 ?? -1; index >= 0; index--) pending.push(current.args[index]);
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
// These defaults mirror SWI-Prolog's JITI admission policy: small predicates
|
|
@@ -110,7 +121,51 @@ export function makeArgumentIndex() {
|
|
|
110
121
|
numberBuckets: new Map(),
|
|
111
122
|
fallback: [],
|
|
112
123
|
sawScalar: false,
|
|
124
|
+
// Dynamic predicates often store fully-ground structured keys such as
|
|
125
|
+
// seen(answer(...)). Keep an exact structural hash alongside the scalar
|
|
126
|
+
// indexes so repeated membership checks do not scan every prior clause.
|
|
127
|
+
groundBuckets: new Map(),
|
|
128
|
+
groundFallback: [],
|
|
129
|
+
sawGround: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function groundTermIndexKey(term, env = null) {
|
|
134
|
+
// Use a compact structural hash instead of materializing the complete term
|
|
135
|
+
// spelling as a Map key. Dynamic bookkeeping predicates frequently store
|
|
136
|
+
// long ground lists; a fixed-size hash avoids retaining another full copy of
|
|
137
|
+
// every list merely for indexing. Hash collisions are harmless because the
|
|
138
|
+
// selected clauses are still matched normally by the solver.
|
|
139
|
+
let hash = 0x811c9dc5;
|
|
140
|
+
const mixCode = (code) => { hash = Math.imul(hash ^ code, 0x01000193) >>> 0; };
|
|
141
|
+
const mixString = (text) => {
|
|
142
|
+
for (let index = 0; index < text.length; index++) mixCode(text.charCodeAt(index));
|
|
143
|
+
mixCode(0xff);
|
|
113
144
|
};
|
|
145
|
+
const stack = [term];
|
|
146
|
+
while (stack.length > 0) {
|
|
147
|
+
const raw = stack.pop();
|
|
148
|
+
const current = env == null ? raw : deref(raw, env);
|
|
149
|
+
if (current?.type === VAR) return null;
|
|
150
|
+
if (current?.type === COMPOUND) {
|
|
151
|
+
mixCode(1);
|
|
152
|
+
mixString(current.name);
|
|
153
|
+
mixCode(current.arity);
|
|
154
|
+
for (let index = current.args.length - 1; index >= 0; index--) stack.push(current.args[index]);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (current?.type === 'number') {
|
|
158
|
+
mixCode(2);
|
|
159
|
+
mixString(numberValueKey(current.name));
|
|
160
|
+
} else if (current?.type === 'string') {
|
|
161
|
+
mixCode(3);
|
|
162
|
+
mixString(String(current.name));
|
|
163
|
+
} else {
|
|
164
|
+
mixCode(4);
|
|
165
|
+
mixString(String(current?.name ?? ''));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return hash;
|
|
114
169
|
}
|
|
115
170
|
|
|
116
171
|
function scalarBuckets(index, term) {
|
|
@@ -168,7 +223,7 @@ export function indexCompactOne(index, type, name, clause, clauses = null, claus
|
|
|
168
223
|
}
|
|
169
224
|
}
|
|
170
225
|
|
|
171
|
-
export function indexOne(index, arg, clause, clauses = null, clausePosition = -1) {
|
|
226
|
+
export function indexOne(index, arg, clause, clauses = null, clausePosition = -1, indexGround = false) {
|
|
172
227
|
if (isScalar(arg)) {
|
|
173
228
|
if (!index.sawScalar) {
|
|
174
229
|
index.sawScalar = true;
|
|
@@ -178,6 +233,18 @@ export function indexOne(index, arg, clause, clauses = null, clausePosition = -1
|
|
|
178
233
|
} else if (index.sawScalar) {
|
|
179
234
|
index.fallback.push(clause);
|
|
180
235
|
}
|
|
236
|
+
|
|
237
|
+
if (!indexGround) return;
|
|
238
|
+
const groundKey = groundTermIndexKey(arg);
|
|
239
|
+
if (groundKey != null) {
|
|
240
|
+
if (!index.sawGround) {
|
|
241
|
+
index.sawGround = true;
|
|
242
|
+
if (clauses && clausePosition > 0) index.groundFallback = clauses.slice(0, clausePosition);
|
|
243
|
+
}
|
|
244
|
+
addClauseBucket(index.groundBuckets, groundKey, clause);
|
|
245
|
+
} else if (index.sawGround) {
|
|
246
|
+
index.groundFallback.push(clause);
|
|
247
|
+
}
|
|
181
248
|
}
|
|
182
249
|
|
|
183
250
|
function indexFallback(index, group) {
|
|
@@ -204,7 +271,9 @@ export function rebuildGroupIndexes(group) {
|
|
|
204
271
|
clause.scalarHead = clause.head.type === COMPOUND && clause.head.args.every(isScalar);
|
|
205
272
|
if (clauseHasCut(clause)) group.hasCut = true;
|
|
206
273
|
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
207
|
-
for (let i = 0; i < group.arity; i++)
|
|
274
|
+
for (let i = 0; i < group.arity; i++) {
|
|
275
|
+
indexOne(group.argIndexes[i], clause.head.args[i], clause, group.clauses, clausePosition, group.dynamic === true);
|
|
276
|
+
}
|
|
208
277
|
}
|
|
209
278
|
}
|
|
210
279
|
|
|
@@ -301,15 +370,36 @@ export function selectClauseCandidates(group, goal, env) {
|
|
|
301
370
|
}
|
|
302
371
|
const positions = [];
|
|
303
372
|
const values = [];
|
|
373
|
+
let bestGround = null;
|
|
374
|
+
let bestGroundLength = group.clauses.length;
|
|
304
375
|
for (let i = 0; i < goal.arity; i++) {
|
|
305
376
|
const arg = deref(goal.args[i], env);
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
377
|
+
if (isScalar(arg)) {
|
|
378
|
+
positions.push(i);
|
|
379
|
+
values.push(arg);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const index = group.argIndexes[i];
|
|
383
|
+
if (!index?.sawGround) continue;
|
|
384
|
+
const key = groundTermIndexKey(arg, env);
|
|
385
|
+
if (key == null) continue;
|
|
386
|
+
const primary = index.groundBuckets.get(key) ?? null;
|
|
387
|
+
const fallback = index.groundFallback;
|
|
388
|
+
const length = clauseCollectionLength(primary) + fallback.length;
|
|
389
|
+
if (length < bestGroundLength) {
|
|
390
|
+
bestGround = fallback.length === 0 ? (primary ?? [])
|
|
391
|
+
: clauseCollectionLength(primary) === 0 ? fallback
|
|
392
|
+
: mergeClausesInSourceOrder(primary, fallback);
|
|
393
|
+
bestGroundLength = length;
|
|
394
|
+
}
|
|
309
395
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
396
|
+
const scalarParts = positions.length === 0 ? null : selectClauseCandidatesForValues(group, positions, values);
|
|
397
|
+
const scalarLength = scalarParts == null
|
|
398
|
+
? group.clauses.length
|
|
399
|
+
: clauseCollectionLength(scalarParts.primary) + scalarParts.fallback.length;
|
|
400
|
+
if (bestGround != null && bestGroundLength < scalarLength) return { primary: bestGround, fallback: [] };
|
|
401
|
+
if (scalarParts != null) return scalarParts;
|
|
402
|
+
return { primary: group.clauses, fallback: [] };
|
|
313
403
|
}
|
|
314
404
|
|
|
315
405
|
// The scalar-fact join already has dereferenced local values. Keeping this
|