eyeprolog 1.4.9 → 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 +9 -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/program-indexing.js +102 -12
- package/src/program.js +70 -12
- 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-regression.mjs +25 -5
- 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,6 +156,7 @@ 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,
|
|
157
161
|
onWarning: printSourceWarning,
|
|
158
162
|
});
|
|
@@ -205,7 +209,10 @@ async function runDefault(engine, program, options) {
|
|
|
205
209
|
const solver = new engine.Solver(program, {
|
|
206
210
|
registry,
|
|
207
211
|
isoStrict: options.isoStrict,
|
|
208
|
-
ioOptions: {
|
|
212
|
+
ioOptions: {
|
|
213
|
+
write: (text) => process.stdout.write(String(text)),
|
|
214
|
+
errorWrite: (text) => process.stderr.write(String(text)),
|
|
215
|
+
},
|
|
209
216
|
});
|
|
210
217
|
program = solver.program;
|
|
211
218
|
const goals = engine.normalizeGoals(options.goals, solver);
|
|
@@ -252,6 +259,7 @@ Options:
|
|
|
252
259
|
reject EyeProlog language extensions and disable automatic tabling.
|
|
253
260
|
--portable Enforce the EyeProlog/Trealla/Scryer interop profile.
|
|
254
261
|
--no-autoload Disable conservative interop predicate autoloading.
|
|
262
|
+
--no-auto-table Use traditional depth-first control instead of automatic recursive tabling.
|
|
255
263
|
-v, --version Show the package version and exit.
|
|
256
264
|
-w, --warnings Print non-fatal portability warnings to stderr.
|
|
257
265
|
-g, --goal goal Solve goal and print its ground answers; may be repeated.
|
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/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
|
package/src/program.js
CHANGED
|
@@ -85,6 +85,9 @@ export class Program {
|
|
|
85
85
|
this.multifilePredicates = new Set();
|
|
86
86
|
this.discontiguousPredicates = new Set();
|
|
87
87
|
this.strictIso = options.isoStrict === true;
|
|
88
|
+
// Normal mode automatically tables recursive user predicates unless an
|
|
89
|
+
// embedder explicitly requests traditional depth-first Prolog control.
|
|
90
|
+
this.autoTabling = options.autoTabling !== false;
|
|
88
91
|
this.operators = new Map();
|
|
89
92
|
const predefinedOperatorSets = this.strictIso
|
|
90
93
|
? [ISO_OPERATOR_DEFINITIONS]
|
|
@@ -177,7 +180,9 @@ export class Program {
|
|
|
177
180
|
group.clauses.push(clause);
|
|
178
181
|
if (clauseHasCut(clause)) group.hasCut = true;
|
|
179
182
|
const clausePosition = group.clauses.length - 1;
|
|
180
|
-
for (let i = 0; i < head.arity; i++)
|
|
183
|
+
for (let i = 0; i < head.arity; i++) {
|
|
184
|
+
indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition, group.dynamic === true);
|
|
185
|
+
}
|
|
181
186
|
}
|
|
182
187
|
findGroup(name, arity, module = 'user') {
|
|
183
188
|
const indicator = `${name}/${arity}`;
|
|
@@ -255,9 +260,24 @@ export class Program {
|
|
|
255
260
|
clause.groundHead = termHasNoVariables(clause.head);
|
|
256
261
|
clause.scalarHead = clause.head.type === COMPOUND && clause.head.args.every(isScalar);
|
|
257
262
|
this.clauses.push(clause);
|
|
258
|
-
if (atStart)
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
if (atStart) {
|
|
264
|
+
group.clauses.unshift(clause);
|
|
265
|
+
rebuildGroupIndexes(group);
|
|
266
|
+
} else {
|
|
267
|
+
const clausePosition = group.clauses.length;
|
|
268
|
+
group.clauses.push(clause);
|
|
269
|
+
// assertz/1 is by far the common dynamic-update path. Appending a clause
|
|
270
|
+
// does not invalidate the existing argument buckets, so extend them in
|
|
271
|
+
// place instead of rebuilding the whole predicate after every insert.
|
|
272
|
+
// This changes repeated ground bookkeeping from quadratic to linear.
|
|
273
|
+
group.demandIndexes.clear();
|
|
274
|
+
group.rejectedDemandIndexes.clear();
|
|
275
|
+
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
276
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
277
|
+
for (let i = 0; i < clause.head.arity; i++) {
|
|
278
|
+
indexOne(group.argIndexes[i], clause.head.args[i], clause, group.clauses, clausePosition, true);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
261
281
|
this.noteMutation(clause.body.length > 0);
|
|
262
282
|
}
|
|
263
283
|
removeDynamicClause(group, clause) {
|
|
@@ -378,7 +398,7 @@ export class Program {
|
|
|
378
398
|
);
|
|
379
399
|
group.listTailRecursive = directRecursiveComponent && !group.cutRecursive &&
|
|
380
400
|
hasStrictListTailRecursion(group);
|
|
381
|
-
group.tabled = plannedRecursive &&
|
|
401
|
+
group.tabled = this.autoTabling && plannedRecursive &&
|
|
382
402
|
!componentHasNegativeEdge(start, deps, negativeEdges) &&
|
|
383
403
|
!group.cutRecursive &&
|
|
384
404
|
!linearNumeric;
|
|
@@ -698,7 +718,7 @@ class ProgramBuilder {
|
|
|
698
718
|
if (clauseHasCut(clause)) group.hasCut = true;
|
|
699
719
|
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
700
720
|
for (let i = 0; i < head.arity; i++) {
|
|
701
|
-
indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
|
|
721
|
+
indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition, group.dynamic === true);
|
|
702
722
|
}
|
|
703
723
|
continue;
|
|
704
724
|
}
|
|
@@ -780,11 +800,13 @@ class ProgramBuilder {
|
|
|
780
800
|
}
|
|
781
801
|
|
|
782
802
|
function buildProgramFromSources(sources, options) {
|
|
783
|
-
// The source-metadata-free path is common for CLI and conformance runs.
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
803
|
+
// The source-metadata-free path is common for CLI and conformance runs. For
|
|
804
|
+
// a homogeneous source set, first try the compact parser directly into a
|
|
805
|
+
// fresh builder. Source sets that contain module-loading directives use a
|
|
806
|
+
// hybrid path instead: directive-heavy sources take the general parser while
|
|
807
|
+
// independent fast-compatible sources can still keep their compact clauses.
|
|
808
|
+
// This matters for launchers that load a small support program followed by a
|
|
809
|
+
// very large ordinary fact/rule file.
|
|
788
810
|
const hasModuleDirectives = sources.some((source) => {
|
|
789
811
|
const text = typeof source === 'string' ? source : source?.text ?? source?.source ?? '';
|
|
790
812
|
return /:-\s*(?:module|use_module)\s*\(/.test(text);
|
|
@@ -793,12 +815,47 @@ function buildProgramFromSources(sources, options) {
|
|
|
793
815
|
const builder = new ProgramBuilder(options);
|
|
794
816
|
if (loadSourcesIntoBuilder(builder, sources, options, true)) return builder.finish();
|
|
795
817
|
}
|
|
818
|
+
if (options.sourceMetadata === false && hasModuleDirectives) {
|
|
819
|
+
const builder = new ProgramBuilder(options);
|
|
820
|
+
if (loadSourcesIntoBuilder(builder, sources, options, 'hybrid')) return builder.finish();
|
|
821
|
+
}
|
|
796
822
|
|
|
797
823
|
const builder = new ProgramBuilder(options);
|
|
798
824
|
loadSourcesIntoBuilder(builder, sources, options, false);
|
|
799
825
|
return builder.finish();
|
|
800
826
|
}
|
|
801
827
|
|
|
828
|
+
function cloneOperatorState(state) {
|
|
829
|
+
return {
|
|
830
|
+
infixOperators: new Map(state.infixOperators),
|
|
831
|
+
prefixOperators: new Map(state.prefixOperators),
|
|
832
|
+
postfixOperators: new Map(state.postfixOperators),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function cloneParserFlagState(state) {
|
|
837
|
+
return {
|
|
838
|
+
doubleQuotes: state.doubleQuotes,
|
|
839
|
+
charConversion: state.charConversion,
|
|
840
|
+
charConversions: new Map(state.charConversions),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function hybridFastSource(source, options) {
|
|
845
|
+
// Module/include directives recurse into additional source texts and can
|
|
846
|
+
// change lexical context. Keep those sources on the general loader. Ordinary
|
|
847
|
+
// user files may still contain op/3, dynamic/1, discontiguous/1, etc.; the
|
|
848
|
+
// compact parser handles those term-by-term and updates the shared state.
|
|
849
|
+
if (/:-(?:\s|\/\*[\s\S]*?\*\/)*(?:module|use_module|include|ensure_loaded)\s*\(/.test(source)) return false;
|
|
850
|
+
const probeOptions = {
|
|
851
|
+
...options,
|
|
852
|
+
operatorState: cloneOperatorState(options.operatorState),
|
|
853
|
+
parserFlagState: cloneParserFlagState(options.parserFlagState),
|
|
854
|
+
onWarning: null,
|
|
855
|
+
};
|
|
856
|
+
return tryParseClausesFastInto(source, () => {}, () => {}, probeOptions);
|
|
857
|
+
}
|
|
858
|
+
|
|
802
859
|
// Program.parse() can see host-supplied goals through autoloadGoals while it
|
|
803
860
|
// builds the program. A Program passed directly to run() has already completed
|
|
804
861
|
// that phase, so extend the existing instance with the same canonical interop
|
|
@@ -863,7 +920,8 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
|
|
|
863
920
|
module: 'user',
|
|
864
921
|
textUnit: sourcePath(item.options) ?? `<input:${sourceIndex}>`,
|
|
865
922
|
};
|
|
866
|
-
|
|
923
|
+
const sourceFast = fast === 'hybrid' ? hybridFastSource(text, item.options) : fast;
|
|
924
|
+
if (!loadSourceIntoBuilder(builder, text, item.options, ensured, loadedModules, sourceFast, context)) return false;
|
|
867
925
|
}
|
|
868
926
|
const autoloadGoals = parseInteropGoalInputs(options.autoloadGoals, {
|
|
869
927
|
...options,
|
package/src/scryer-compat.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// backtrackable blackboard lives in Env so ordinary Prolog choice points undo
|
|
5
5
|
// writes automatically.
|
|
6
6
|
|
|
7
|
-
import { ATOM, COMPOUND, NUMBER, STRING, VAR, atom, compound, deref, listFromItems, unify } from './term.js';
|
|
7
|
+
import { ATOM, COMPOUND, NUMBER, STRING, VAR, atom, compound, deref, listFromItems, numberTerm, numberTextFromDouble, unify } from './term.js';
|
|
8
8
|
import { PrologError } from './errors.js';
|
|
9
9
|
import { formatTermForWrite } from './write.js';
|
|
10
10
|
|
|
@@ -46,6 +46,27 @@ function* bbGlobalPutBuiltin({ solver, goal, env }) {
|
|
|
46
46
|
yield env;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
function* randomValueBuiltin({ solver, goal, env }) {
|
|
52
|
+
const key = 'a:$random_seed';
|
|
53
|
+
const stored = solver.nonBacktrackableBlackboard.get(key);
|
|
54
|
+
let seed = stored == null ? 1 : Number(deref(stored, env).name);
|
|
55
|
+
if (!Number.isSafeInteger(seed)) seed = 1;
|
|
56
|
+
seed %= 2147483647;
|
|
57
|
+
if (seed < 0) seed += 2147483647;
|
|
58
|
+
if (seed === 0) seed = 1;
|
|
59
|
+
const high = Math.floor(seed / 44488);
|
|
60
|
+
const low = seed % 44488;
|
|
61
|
+
let nextSeed = 48271 * low - 3399 * high;
|
|
62
|
+
if (nextSeed <= 0) nextSeed += 2147483647;
|
|
63
|
+
const value = (nextSeed - 1) / 2147483646;
|
|
64
|
+
const next = env.clone();
|
|
65
|
+
if (!unify(goal.args[0], numberTerm(numberTextFromDouble(value)), next)) return;
|
|
66
|
+
solver.nonBacktrackableBlackboard.set(key, numberTerm(nextSeed));
|
|
67
|
+
yield next;
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
const charTypeNames = new Set([
|
|
50
71
|
'alnum', 'alpha', 'alphabetic', 'alphanumeric', 'ascii', 'ascii_graphic',
|
|
51
72
|
'ascii_punctuation', 'binary_digit', 'control', 'decimal_digit', 'exponent',
|
|
@@ -215,15 +236,34 @@ function* termCharsBuiltin({ solver, goal, env }) {
|
|
|
215
236
|
if (unify(goal.args[2], chars(text), next)) yield next;
|
|
216
237
|
}
|
|
217
238
|
|
|
239
|
+
|
|
240
|
+
function* portrayClauseBuiltin({ solver, goal, env }) {
|
|
241
|
+
const text = formatTermForWrite(goal.args[0], env, {
|
|
242
|
+
quoted: true,
|
|
243
|
+
numbervars: true,
|
|
244
|
+
compact: true,
|
|
245
|
+
minimalOperatorSpacing: true,
|
|
246
|
+
operatorAtomsAsArgs: true,
|
|
247
|
+
generateVariableNames: true,
|
|
248
|
+
variableNameState: solver.writeVariableState,
|
|
249
|
+
operators: solver.program.operators.values(),
|
|
250
|
+
});
|
|
251
|
+
const stream = solver.io.resolve(solver.io.currentOutput);
|
|
252
|
+
solver.io.writeUnit(stream, `${text}.\n`);
|
|
253
|
+
yield env;
|
|
254
|
+
}
|
|
255
|
+
|
|
218
256
|
export const scryerCompatibilityBuiltins = {
|
|
219
257
|
register(registry) {
|
|
220
258
|
registry.add('eyeprolog__bb_get', 2, bbGetBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
221
259
|
registry.add('eyeprolog__bb_b_put', 2, bbPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
222
260
|
registry.add('eyeprolog__bb_global_get', 2, bbGlobalGetBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
223
261
|
registry.add('eyeprolog__bb_global_put', 2, bbGlobalPutBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
262
|
+
registry.add('eyeprolog__random_value', 1, randomValueBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
224
263
|
registry.add('eyeprolog__char_type', 2, charTypeBuiltin, { eyePrologLibrary: true });
|
|
225
264
|
registry.add('eyeprolog__current_time', 1, currentTimeBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
226
265
|
registry.add('eyeprolog__abolish_all_tables', 0, abolishAllTablesBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
227
266
|
registry.add('eyeprolog__term_chars', 3, termCharsBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
267
|
+
registry.add('portray_clause', 1, portrayClauseBuiltin, { deterministic: true, eyePrologLibrary: true });
|
|
228
268
|
},
|
|
229
269
|
};
|
package/src/solver.js
CHANGED
|
@@ -17,6 +17,7 @@ import { evaluatePositiveDatalog, relationForDatalogGroup, datalogCandidateIndex
|
|
|
17
17
|
|
|
18
18
|
let freshCounter = 0;
|
|
19
19
|
const DEFAULT_INNER_TABLE_SCOPE_LIMIT = 1024;
|
|
20
|
+
const GOAL_CONTINUATION_THRESHOLD = 64;
|
|
20
21
|
// Conservative live-storage estimate for one generated length/2 list cell
|
|
21
22
|
// (cons object, argument vector, fresh variable, and its generated name).
|
|
22
23
|
const GENERATED_LENGTH_CELL_RESERVE_BYTES = 256;
|
|
@@ -438,8 +439,22 @@ export class Solver {
|
|
|
438
439
|
if (frame.kind === 'userClause') {
|
|
439
440
|
if (this.solutionsSeen >= this.solutionLimit) continue;
|
|
440
441
|
const { clause, goal: clauseGoal, rest: clauseRest, env: clauseEnv, depth: clauseDepth,
|
|
441
|
-
active: clauseActive, release } = frame;
|
|
442
|
+
active: clauseActive, release, tailReleaseBeforeLast, directCutReleaseIndex } = frame;
|
|
442
443
|
let next;
|
|
444
|
+
if (clause.body.length === 0 && clause.groundHead) {
|
|
445
|
+
const matched = sameResolvedGroundTerm(clauseGoal, clause.head, clauseEnv);
|
|
446
|
+
if (matched === true) {
|
|
447
|
+
stack.push({
|
|
448
|
+
kind: 'goals',
|
|
449
|
+
goals: [...release, ...clauseRest],
|
|
450
|
+
env: clauseEnv,
|
|
451
|
+
depth: clauseDepth + 1,
|
|
452
|
+
active: clauseActive,
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (matched === false) continue;
|
|
457
|
+
}
|
|
443
458
|
if (clause.body.length === 0 && clause.scalarHead) {
|
|
444
459
|
next = matchScalarFact(clauseGoal, clause.head, clauseEnv);
|
|
445
460
|
if (!next) continue;
|
|
@@ -463,11 +478,33 @@ export class Solver {
|
|
|
463
478
|
next = clauseEnv.clone();
|
|
464
479
|
this.stats.unify_calls++;
|
|
465
480
|
if (!unify(clauseGoal, freshHead, next, { knownNonoccurringVariables: headLocalFresh })) continue;
|
|
481
|
+
let nextGoals;
|
|
482
|
+
if (freshBody.length === 0) {
|
|
483
|
+
nextGoals = [...release, ...clauseRest];
|
|
484
|
+
} else if (directCutReleaseIndex >= 0) {
|
|
485
|
+
const afterCut = directCutReleaseIndex + 1;
|
|
486
|
+
nextGoals = [...freshBody.slice(0, afterCut), ...release, ...freshBody.slice(afterCut), ...clauseRest];
|
|
487
|
+
} else if (tailReleaseBeforeLast) {
|
|
488
|
+
const tail = freshBody[freshBody.length - 1];
|
|
489
|
+
next.compactForDeepContinuation?.();
|
|
490
|
+
nextGoals = [...freshBody.slice(0, -1), ...release, tail, ...clauseRest];
|
|
491
|
+
} else if (clauseRest.length >= GOAL_CONTINUATION_THRESHOLD ||
|
|
492
|
+
clauseRest.some((goal) => goal?.kind === 'continueGoals')) {
|
|
493
|
+
// Keep a large caller continuation as one opaque frame instead of
|
|
494
|
+
// copying its complete pending goal list into every nested clause.
|
|
495
|
+
// Small goal lists remain flat because that representation is faster
|
|
496
|
+
// for ordinary shallow search.
|
|
497
|
+
next.compactForDeepContinuation?.();
|
|
498
|
+
nextGoals = [
|
|
499
|
+
...freshBody,
|
|
500
|
+
{ kind: 'continueGoals', goals: clauseRest, depth: clauseDepth, releaseActive: release.length !== 0 },
|
|
501
|
+
];
|
|
502
|
+
} else {
|
|
503
|
+
nextGoals = [...freshBody, ...release, ...clauseRest];
|
|
504
|
+
}
|
|
466
505
|
stack.push({
|
|
467
506
|
kind: 'goals',
|
|
468
|
-
goals:
|
|
469
|
-
? [...release, ...clauseRest]
|
|
470
|
-
: [...freshBody, ...release, ...clauseRest],
|
|
507
|
+
goals: nextGoals,
|
|
471
508
|
env: next,
|
|
472
509
|
depth: clauseDepth + 1,
|
|
473
510
|
active: clauseActive,
|
|
@@ -552,6 +589,12 @@ export class Solver {
|
|
|
552
589
|
}
|
|
553
590
|
|
|
554
591
|
const first = goals[0];
|
|
592
|
+
if (first?.kind === 'continueGoals') {
|
|
593
|
+
if (first.releaseActive) active = active.slice(0, -1);
|
|
594
|
+
depth = first.depth;
|
|
595
|
+
goals = first.goals;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
555
598
|
if (first?.kind === 'releaseActive') {
|
|
556
599
|
active = active.slice(0, -1);
|
|
557
600
|
goals = goals.slice(1);
|
|
@@ -987,7 +1030,7 @@ export class Solver {
|
|
|
987
1030
|
}
|
|
988
1031
|
|
|
989
1032
|
*solveUserGoalUncached(group, goal, rest, env, depth) {
|
|
990
|
-
if (group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) {
|
|
1033
|
+
if (this.program.autoTabling !== false && group.recursive && !group.cutRecursive && !group.linearNumeric && this.activeVariant(goal, env)) {
|
|
991
1034
|
this.recursionCycleDetected = true;
|
|
992
1035
|
return;
|
|
993
1036
|
}
|
|
@@ -1194,7 +1237,7 @@ function pushMemoAnswerFrames(stack, entry, goal, rest, env, depth, active, solv
|
|
|
1194
1237
|
}
|
|
1195
1238
|
|
|
1196
1239
|
function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
1197
|
-
if (group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) {
|
|
1240
|
+
if (solver.program.autoTabling !== false && group.recursive && !group.cutRecursive && !group.linearNumeric && activeVariantIn(goal, env, active)) {
|
|
1198
1241
|
solver.recursionCycleDetected = true;
|
|
1199
1242
|
return;
|
|
1200
1243
|
}
|
|
@@ -1208,9 +1251,6 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
1208
1251
|
// recursive user predicates. Cut-free, non-recursive library helpers need
|
|
1209
1252
|
// neither. Copying their full active stack at every recursive step made
|
|
1210
1253
|
// otherwise linear relations such as length/2 retain O(depth^2) references.
|
|
1211
|
-
const guarded = groupNeedsActiveFrame(group);
|
|
1212
|
-
const release = guarded ? [{ kind: 'releaseActive' }] : [];
|
|
1213
|
-
const nextActive = guarded ? [...active, invocation] : active;
|
|
1214
1254
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
1215
1255
|
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(pass); candidateIndex++) {
|
|
1216
1256
|
const clause = clauseCandidateAt(pass, candidateIndex);
|
|
@@ -1226,11 +1266,26 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
1226
1266
|
rest,
|
|
1227
1267
|
env,
|
|
1228
1268
|
depth,
|
|
1229
|
-
active: nextActive,
|
|
1230
|
-
release,
|
|
1231
1269
|
});
|
|
1232
1270
|
}
|
|
1233
1271
|
}
|
|
1272
|
+
// Traditional depth-first mode disables recursive-cycle guards. A cut
|
|
1273
|
+
// boundary is then needed only if an actually reachable clause contains a
|
|
1274
|
+
// cut. Scalar indexes often narrow a mixed predicate to one cut-free
|
|
1275
|
+
// recursive clause; do not copy the whole active path at every such layer.
|
|
1276
|
+
const guarded = solver.program.autoTabling === false
|
|
1277
|
+
? frames.some((frame) => clauseHasCutForTailRelease(frame.clause))
|
|
1278
|
+
: groupNeedsActiveFrame(group);
|
|
1279
|
+
const release = guarded ? [{ kind: 'releaseActive' }] : [];
|
|
1280
|
+
const nextActive = guarded ? [...active, invocation] : active;
|
|
1281
|
+
for (const frame of frames) {
|
|
1282
|
+
frame.active = nextActive;
|
|
1283
|
+
frame.release = release;
|
|
1284
|
+
frame.tailReleaseBeforeLast = solver.program.autoTabling === false && guarded &&
|
|
1285
|
+
clauseCanReleaseActiveBeforeTailCall(group, frame.clause);
|
|
1286
|
+
frame.directCutReleaseIndex = solver.program.autoTabling === false && guarded
|
|
1287
|
+
? clauseDirectCutReleaseIndex(frame.clause) : -1;
|
|
1288
|
+
}
|
|
1234
1289
|
for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
|
|
1235
1290
|
}
|
|
1236
1291
|
|
|
@@ -1363,6 +1418,51 @@ function prepareLocalVariablesForGoal(goal, env) {
|
|
|
1363
1418
|
env.markLocalVariables(goal?._localFirstUseVariables ?? null);
|
|
1364
1419
|
}
|
|
1365
1420
|
|
|
1421
|
+
function clauseDirectCutReleaseIndex(clause) {
|
|
1422
|
+
if (clause?.compactBinary === true) return -1;
|
|
1423
|
+
let lastDirectCut = -1;
|
|
1424
|
+
for (let index = 0; index < clause.body.length; index++) {
|
|
1425
|
+
const goal = clause.body[index];
|
|
1426
|
+
if (goal?.type === ATOM && goal.name === '!') {
|
|
1427
|
+
lastDirectCut = index;
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
if (termContainsCutForRelease(goal)) return -1;
|
|
1431
|
+
}
|
|
1432
|
+
return lastDirectCut;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function termContainsCutForRelease(term) {
|
|
1436
|
+
if (term?.type === ATOM) return term.name === '!';
|
|
1437
|
+
if (term?.type !== COMPOUND) return false;
|
|
1438
|
+
const pending = [...term.args];
|
|
1439
|
+
while (pending.length > 0) {
|
|
1440
|
+
const current = pending.pop();
|
|
1441
|
+
if (current?.type === ATOM && current.name === '!') return true;
|
|
1442
|
+
if (current?.type === COMPOUND) for (const arg of current.args) pending.push(arg);
|
|
1443
|
+
}
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function clauseCanReleaseActiveBeforeTailCall(group, clause) {
|
|
1448
|
+
if (clauseHasCutForTailRelease(clause)) return false;
|
|
1449
|
+
if (group.clauses[group.clauses.length - 1] !== clause || clause.body.length === 0) return false;
|
|
1450
|
+
const tail = clause.body[clause.body.length - 1];
|
|
1451
|
+
return tail?.type === COMPOUND && tail.name === group.name && tail.arity === group.arity &&
|
|
1452
|
+
(tail.module ?? group.module) === group.module;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function clauseHasCutForTailRelease(clause) {
|
|
1456
|
+
if (clause?.compactBinary === true) return false;
|
|
1457
|
+
const pending = [...clause.body];
|
|
1458
|
+
while (pending.length > 0) {
|
|
1459
|
+
const current = pending.pop();
|
|
1460
|
+
if (current?.type === ATOM && current.name === '!') return true;
|
|
1461
|
+
if (current?.type === COMPOUND) for (const arg of current.args) pending.push(arg);
|
|
1462
|
+
}
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1366
1466
|
function groupNeedsActiveFrame(group) {
|
|
1367
1467
|
// A direct recursive call that consumes the tail of a matched list cannot
|
|
1368
1468
|
// revisit an earlier finite-tree call. It needs neither a cycle guard nor an
|
|
@@ -2174,7 +2274,7 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
2174
2274
|
const groups = [];
|
|
2175
2275
|
while (runLength < goals.length) {
|
|
2176
2276
|
const goal = goals[runLength];
|
|
2177
|
-
if (!goal || goal.kind === 'releaseActive' || goal.kind === 'memoStore') break;
|
|
2277
|
+
if (!goal || goal.kind === 'releaseActive' || goal.kind === 'memoStore' || goal.kind === 'continueGoals') break;
|
|
2178
2278
|
if (goal.type !== COMPOUND) break;
|
|
2179
2279
|
const def = solver.registry.get(goal.name, goal.arity);
|
|
2180
2280
|
if (def) break;
|
|
@@ -2214,7 +2314,7 @@ function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
|
2214
2314
|
}
|
|
2215
2315
|
|
|
2216
2316
|
const goal = runGoals[state.index];
|
|
2217
|
-
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
|
|
2317
|
+
if (solver.program.autoTabling !== false && activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
|
|
2218
2318
|
solver.recursionCycleDetected = true;
|
|
2219
2319
|
continue;
|
|
2220
2320
|
}
|
|
@@ -2274,7 +2374,7 @@ function* scalarFactRunGenerator(solver, goals, groups, env, depth, active, pend
|
|
|
2274
2374
|
}
|
|
2275
2375
|
|
|
2276
2376
|
const goal = goals[state.index];
|
|
2277
|
-
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
|
|
2377
|
+
if (solver.program.autoTabling !== false && activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) {
|
|
2278
2378
|
solver.recursionCycleDetected = true;
|
|
2279
2379
|
continue;
|
|
2280
2380
|
}
|
|
@@ -2433,19 +2533,72 @@ function compactIndexBucket(index, type, name) {
|
|
|
2433
2533
|
}
|
|
2434
2534
|
|
|
2435
2535
|
function tryPushCompactBinaryChainFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
2436
|
-
if (
|
|
2437
|
-
// This fast path
|
|
2438
|
-
//
|
|
2439
|
-
//
|
|
2440
|
-
//
|
|
2536
|
+
if (goal.type !== COMPOUND || goal.arity !== 2) return false;
|
|
2537
|
+
// This fast path follows deterministic compact binary chains selected by a
|
|
2538
|
+
// scalar second argument. In addition to a scalar first argument, accept an
|
|
2539
|
+
// unbound first variable when every recursive rule carries that variable
|
|
2540
|
+
// through unchanged. The latter is the common taxonomy form p(X,nN) :-
|
|
2541
|
+
// p(X,nN-1): it can be walked iteratively and X is bound only by the base
|
|
2542
|
+
// fact, avoiding thousands of recursive Env/active-frame allocations.
|
|
2441
2543
|
const first = derefForLocal(goal.args[0], env);
|
|
2442
2544
|
const second = derefForLocal(goal.args[1], env);
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
if (!
|
|
2545
|
+
const firstScalar = isScalarTerm(first);
|
|
2546
|
+
const firstVariable = first?.type === 'var';
|
|
2547
|
+
if ((!firstScalar && !firstVariable) || !['atom', 'string', 'number'].includes(second?.type)) return false;
|
|
2548
|
+
// Preserve the established cached scalar-chain semantics inside an active
|
|
2549
|
+
// invocation. The variable-carry chain below does not use cross-call caches
|
|
2550
|
+
// and is safe in an active meta-call because it still checks its own cycles.
|
|
2551
|
+
if (firstScalar && active.length !== 0) return false;
|
|
2446
2552
|
|
|
2447
2553
|
const index = group.argIndexes[1];
|
|
2448
2554
|
if (!index?.sawScalar || index.fallback.length !== 0) return false;
|
|
2555
|
+
|
|
2556
|
+
if (firstVariable) {
|
|
2557
|
+
const seen = scalarSetContainer();
|
|
2558
|
+
let secondType = second.type;
|
|
2559
|
+
let secondName = second.name;
|
|
2560
|
+
let currentDepth = depth;
|
|
2561
|
+
while (true) {
|
|
2562
|
+
if (solver.solutionsSeen >= solver.solutionLimit) return true;
|
|
2563
|
+
solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
|
|
2564
|
+
const seenSet = seen[secondType];
|
|
2565
|
+
if (!seenSet) return true;
|
|
2566
|
+
if (seenSet.has(secondName)) {
|
|
2567
|
+
solver.recursionCycleDetected = true;
|
|
2568
|
+
return true;
|
|
2569
|
+
}
|
|
2570
|
+
seenSet.add(secondName);
|
|
2571
|
+
|
|
2572
|
+
const candidates = compactIndexBucket(index, secondType, secondName);
|
|
2573
|
+
if (clauseCandidateLength(candidates) !== 1) return false;
|
|
2574
|
+
const clause = clauseCandidateAt(candidates, 0);
|
|
2575
|
+
if (clause?.compactBinary !== true || clause.headName !== group.name) return false;
|
|
2576
|
+
if (clause.head1Type !== secondType || clause.head1Name !== secondName) return true;
|
|
2577
|
+
|
|
2578
|
+
if (clause.bodyName == null) {
|
|
2579
|
+
const next = env.clone();
|
|
2580
|
+
// A variable base fact leaves the carried query variable unconstrained;
|
|
2581
|
+
// a scalar base fact binds it exactly as ordinary clause unification.
|
|
2582
|
+
if (clause.head0Type !== 'var') {
|
|
2583
|
+
if (!unify(goal.args[0], clause.head.args[0], next)) return true;
|
|
2584
|
+
if (next._variableConstraints != null && !next.validateVariableConstraints()) return true;
|
|
2585
|
+
}
|
|
2586
|
+
stack.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
if (clause.bodyName !== group.name || clause.head0Type !== 'var' ||
|
|
2591
|
+
clause.body0Type !== 'var' || clause.body0Name !== clause.head0Name ||
|
|
2592
|
+
!['atom', 'string', 'number'].includes(clause.body1Type)) return false;
|
|
2593
|
+
|
|
2594
|
+
secondType = clause.body1Type;
|
|
2595
|
+
secondName = clause.body1Name;
|
|
2596
|
+
currentDepth++;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
let secondType = second.type;
|
|
2601
|
+
let secondName = second.name;
|
|
2449
2602
|
const cache = compactChainCacheFor(solver, group, first);
|
|
2450
2603
|
const seen = scalarSetContainer();
|
|
2451
2604
|
let currentDepth = depth;
|
|
@@ -2590,7 +2743,7 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
|
|
|
2590
2743
|
solver.recursionCycleDetected = true;
|
|
2591
2744
|
return true;
|
|
2592
2745
|
}
|
|
2593
|
-
if (activeVariantIn(currentGoal, currentEnv, active)) {
|
|
2746
|
+
if (solver.program.autoTabling !== false && activeVariantIn(currentGoal, currentEnv, active)) {
|
|
2594
2747
|
solver.recursionCycleDetected = true;
|
|
2595
2748
|
return true;
|
|
2596
2749
|
}
|
|
@@ -2741,11 +2894,30 @@ function sameScalarTerm(left, right) {
|
|
|
2741
2894
|
}
|
|
2742
2895
|
|
|
2743
2896
|
function sameGroundTerm(left, right) {
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2897
|
+
const pending = [[left, right]];
|
|
2898
|
+
while (pending.length > 0) {
|
|
2899
|
+
const [a, b] = pending.pop();
|
|
2900
|
+
if (a?.type !== b?.type) return false;
|
|
2901
|
+
if (a?.type === 'number' ? !sameNumberValue(a.name, b.name) : a?.name !== b?.name) return false;
|
|
2902
|
+
const arity = a?.args?.length ?? 0;
|
|
2903
|
+
if (arity !== (b?.args?.length ?? 0)) return false;
|
|
2904
|
+
for (let index = 0; index < arity; index++) pending.push([a.args[index], b.args[index]]);
|
|
2905
|
+
}
|
|
2906
|
+
return true;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function sameResolvedGroundTerm(left, right, env) {
|
|
2910
|
+
const pending = [[left, right]];
|
|
2911
|
+
while (pending.length > 0) {
|
|
2912
|
+
let [a, b] = pending.pop();
|
|
2913
|
+
a = derefForLocal(a, env);
|
|
2914
|
+
if (a?.type === 'var') return null;
|
|
2915
|
+
if (a?.type !== b?.type) return false;
|
|
2916
|
+
if (a?.type === 'number' ? !sameNumberValue(a.name, b.name) : a?.name !== b?.name) return false;
|
|
2917
|
+
const arity = a?.args?.length ?? 0;
|
|
2918
|
+
if (arity !== (b?.args?.length ?? 0)) return false;
|
|
2919
|
+
for (let index = 0; index < arity; index++) pending.push([a.args[index], b.args[index]]);
|
|
2920
|
+
}
|
|
2749
2921
|
return true;
|
|
2750
2922
|
}
|
|
2751
2923
|
|
|
@@ -2908,7 +3080,7 @@ function pushResumeBuiltinFrame(stack, iterator, goals, depth, active) {
|
|
|
2908
3080
|
function selectReadyDeterministicBuiltin(goals, env, registry) {
|
|
2909
3081
|
for (let i = 0; i < goals.length; i++) {
|
|
2910
3082
|
const goal = goals[i];
|
|
2911
|
-
if (goal?.kind === 'releaseActive' || goal?.kind === 'memoStore') return 0;
|
|
3083
|
+
if (goal?.kind === 'releaseActive' || goal?.kind === 'memoStore' || goal?.kind === 'continueGoals') return 0;
|
|
2912
3084
|
// A first-use proof is derived from source goal order. Do not move a later
|
|
2913
3085
|
// deterministic builtin across that equality: doing so could touch one of
|
|
2914
3086
|
// its proven-fresh variables before the checked binding executes.
|
package/src/standard-library.js
CHANGED
|
@@ -84,6 +84,7 @@ export const eyePrologNativeLibraryIndicators = Object.freeze([
|
|
|
84
84
|
'disjoint2/1', 'automaton/3', 'automaton/8', 'chain/2', 'element/3', 'zcompare/3',
|
|
85
85
|
'fd_var/1', 'fd_inf/2', 'fd_sup/2', 'fd_size/2', 'fd_dom/2',
|
|
86
86
|
'clpz_t/2', '#=/3', '#</3',
|
|
87
|
+
'random/1',
|
|
87
88
|
]);
|
|
88
89
|
export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
89
90
|
'sumall/3', 'aggregate_min/5', 'aggregate_max/5',
|
|
@@ -232,7 +233,7 @@ const eyePrologSharedLibraryIndicators = [
|
|
|
232
233
|
'ord_subtract/3', 'ord_symdiff/3', 'ord_union/2', 'ord_union/3', 'ord_union/4',
|
|
233
234
|
'phrase_from_file/2', 'phrase_from_file/3', 'phrase_to_file/2',
|
|
234
235
|
'phrase_to_file/3', 'phrase_to_stream/2',
|
|
235
|
-
'maybe/0', 'random_integer/3', 'set_random/1',
|
|
236
|
+
'maybe/0', 'random/1', 'random_integer/3', 'set_random/1',
|
|
236
237
|
',/3', ';/3', '=/3', 'cond_t/3', 'dif/3', 'if_/3', 'memberd_t/3',
|
|
237
238
|
'tfilter/3', 'tmember/2', 'tmember_t/3', 'tpartition/4',
|
|
238
239
|
'start_tabling/2', 'abolish_all_tables/0', 'current_time/1', 'format_time/4',
|
package/src/term.js
CHANGED
|
@@ -13,7 +13,7 @@ const EMPTY_ARGS = Object.freeze([]);
|
|
|
13
13
|
// the chain into its indexed Map before that lookup cost dominates. Ordinary
|
|
14
14
|
// execution keeps the memory-friendly 512-layer threshold; attributed-variable
|
|
15
15
|
// propagation uses 256 because its repeated negative lookups dominate sooner.
|
|
16
|
-
const ENV_FLATTEN_DEPTH =
|
|
16
|
+
const ENV_FLATTEN_DEPTH = 256;
|
|
17
17
|
const ATTRIBUTED_ENV_FLATTEN_DEPTH = 256;
|
|
18
18
|
// Runtime terms are structurally immutable: environments hold bindings beside
|
|
19
19
|
// them rather than rewriting their argument arrays. Cache only the syntactic
|
|
@@ -105,6 +105,7 @@ export class Env {
|
|
|
105
105
|
bindingValue: undefined,
|
|
106
106
|
parent: null,
|
|
107
107
|
depth: 0,
|
|
108
|
+
segmentCount: 0,
|
|
108
109
|
cacheName: null,
|
|
109
110
|
cacheValue: undefined,
|
|
110
111
|
cache: null,
|
|
@@ -318,29 +319,55 @@ export class Env {
|
|
|
318
319
|
}
|
|
319
320
|
return undefined;
|
|
320
321
|
}
|
|
322
|
+
compactForDeepContinuation(segmentLimit = 16) {
|
|
323
|
+
if ((this._state.segmentCount ?? 0) < segmentLimit) return false;
|
|
324
|
+
const flattened = new Map();
|
|
325
|
+
for (let state = this._state; state != null; state = state.parent) {
|
|
326
|
+
if (state.bindingName != null && !flattened.has(state.bindingName)) {
|
|
327
|
+
flattened.set(state.bindingName, state.bindingValue);
|
|
328
|
+
}
|
|
329
|
+
if (state.bindings) {
|
|
330
|
+
for (const [key, value] of state.bindings) {
|
|
331
|
+
if (!flattened.has(key)) flattened.set(key, value);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
this._state = {
|
|
336
|
+
bindings: flattened,
|
|
337
|
+
bindingName: null,
|
|
338
|
+
bindingValue: undefined,
|
|
339
|
+
parent: null,
|
|
340
|
+
depth: 0,
|
|
341
|
+
segmentCount: 0,
|
|
342
|
+
cacheName: null,
|
|
343
|
+
cacheValue: undefined,
|
|
344
|
+
cache: null,
|
|
345
|
+
};
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
321
348
|
bind(name, term) {
|
|
322
349
|
const flattenDepth = this._prologAttributes == null
|
|
323
350
|
? ENV_FLATTEN_DEPTH
|
|
324
351
|
: ATTRIBUTED_ENV_FLATTEN_DEPTH;
|
|
325
352
|
if (this._state.depth >= flattenDepth) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
353
|
+
// Compact only the newest single-binding segment. Older compacted
|
|
354
|
+
// segments remain linked as parents, so deep deterministic recursion
|
|
355
|
+
// never recopies its complete binding history at every threshold.
|
|
356
|
+
const segment = new Map([[name, term]]);
|
|
357
|
+
let state = this._state;
|
|
358
|
+
while (state != null && state.bindings == null) {
|
|
359
|
+
if (state.bindingName != null && !segment.has(state.bindingName)) {
|
|
360
|
+
segment.set(state.bindingName, state.bindingValue);
|
|
335
361
|
}
|
|
362
|
+
state = state.parent;
|
|
336
363
|
}
|
|
337
|
-
flattened.set(name, term);
|
|
338
364
|
this._state = {
|
|
339
|
-
bindings:
|
|
365
|
+
bindings: segment,
|
|
340
366
|
bindingName: null,
|
|
341
367
|
bindingValue: undefined,
|
|
342
|
-
parent:
|
|
368
|
+
parent: state,
|
|
343
369
|
depth: 0,
|
|
370
|
+
segmentCount: (state?.segmentCount ?? 0) + 1,
|
|
344
371
|
cacheName: null,
|
|
345
372
|
cacheValue: undefined,
|
|
346
373
|
cache: null,
|
|
@@ -353,6 +380,7 @@ export class Env {
|
|
|
353
380
|
bindingValue: term,
|
|
354
381
|
parent: this._state,
|
|
355
382
|
depth: this._state.depth + 1,
|
|
383
|
+
segmentCount: this._state.segmentCount ?? 0,
|
|
356
384
|
cacheName: null,
|
|
357
385
|
cacheValue: undefined,
|
|
358
386
|
cache: null,
|
|
@@ -923,12 +951,40 @@ export function freshTerm(term, suffix, variables = new Map()) {
|
|
|
923
951
|
}
|
|
924
952
|
|
|
925
953
|
export function copyResolved(term, env) {
|
|
954
|
+
const makeCopy = (resolved) => {
|
|
955
|
+
if (resolved.type === VAR) return variable(resolved.name);
|
|
956
|
+
const copied = resolved.type === COMPOUND && resolved.arity === 0
|
|
957
|
+
? atom(resolved.name)
|
|
958
|
+
: new Term(resolved.type, resolved.name, new Array(resolved.args.length));
|
|
959
|
+
if (resolved.module != null) copied.module = resolved.module;
|
|
960
|
+
return copied;
|
|
961
|
+
};
|
|
962
|
+
|
|
926
963
|
const resolved = deref(term, env);
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
964
|
+
const copied = makeCopy(resolved);
|
|
965
|
+
if (resolved.type === VAR || resolved.args.length === 0) return copied;
|
|
966
|
+
|
|
967
|
+
// Deep lists and machine-state terms can contain thousands of nested cells.
|
|
968
|
+
// Copy them iteratively so readback never consumes the JavaScript call stack.
|
|
969
|
+
// Keep a source-to-copy map as well, both to preserve shared subterms and to
|
|
970
|
+
// terminate on rational trees when occurs_check is disabled.
|
|
971
|
+
const copies = new Map([[resolved, copied]]);
|
|
972
|
+
const pending = [{ source: resolved, target: copied }];
|
|
973
|
+
while (pending.length > 0) {
|
|
974
|
+
const { source, target } = pending.pop();
|
|
975
|
+
for (let index = 0; index < source.args.length; index++) {
|
|
976
|
+
const childSource = deref(source.args[index], env);
|
|
977
|
+
let childCopy = copies.get(childSource);
|
|
978
|
+
if (childCopy == null) {
|
|
979
|
+
childCopy = makeCopy(childSource);
|
|
980
|
+
if (childSource.type !== VAR && childSource.args.length > 0) {
|
|
981
|
+
copies.set(childSource, childCopy);
|
|
982
|
+
pending.push({ source: childSource, target: childCopy });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
target.args[index] = childCopy;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
932
988
|
return copied;
|
|
933
989
|
}
|
|
934
990
|
|
package/test/run-regression.mjs
CHANGED
|
@@ -4042,6 +4042,23 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
|
|
|
4042
4042
|
assertEqual(result.stderr, '', 'stderr');
|
|
4043
4043
|
},
|
|
4044
4044
|
},
|
|
4045
|
+
{
|
|
4046
|
+
name: 'traditional depth-first mode keeps deep non-tail continuations bounded',
|
|
4047
|
+
run: () => {
|
|
4048
|
+
const source = `
|
|
4049
|
+
countdown(0, 0) :- !.
|
|
4050
|
+
countdown(N, Result) :-
|
|
4051
|
+
Next is N-1,
|
|
4052
|
+
countdown(Next, Partial),
|
|
4053
|
+
Result is Partial+1.
|
|
4054
|
+
answer(Result) :- countdown(2048, Result), Result = 2048.
|
|
4055
|
+
`;
|
|
4056
|
+
const result = run(source, { goal: 'answer(Result)', autoTabling: false });
|
|
4057
|
+
assertEqual(result.stdout, 'answer(2048).\n', 'deep non-tail answer');
|
|
4058
|
+
assertEqual(result.stats.max_goal_count <= 70, true,
|
|
4059
|
+
'caller continuations stay opaque instead of growing one goal per recursive layer');
|
|
4060
|
+
},
|
|
4061
|
+
},
|
|
4045
4062
|
{
|
|
4046
4063
|
name: '--portable rejects implementation-specific library dependencies',
|
|
4047
4064
|
run: () => {
|
|
@@ -6126,7 +6143,9 @@ answer(ok) :-
|
|
|
6126
6143
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
6127
6144
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
6128
6145
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
6129
|
-
assertEqual(library.defs.size,
|
|
6146
|
+
assertEqual(library.defs.size, 163, 'EyeProlog registry contains ISO definitions, cleanup controls, observability extensions, WFS tnot/1, and generic library adapters');
|
|
6147
|
+
assertEqual(registry.get('eyeprolog__random_value', 1), null, 'native random helper is absent from the ISO registry');
|
|
6148
|
+
assertEqual(Boolean(library.get('eyeprolog__random_value', 1)), true, 'native random helper is an internal EyeProlog library adapter');
|
|
6130
6149
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
6131
6150
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
6132
6151
|
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
@@ -6143,9 +6162,9 @@ answer(ok) :-
|
|
|
6143
6162
|
assertEqual(Boolean(library.get('call_cleanup', 2)), true, 'call_cleanup/2 is an EyeProlog cleanup control');
|
|
6144
6163
|
assertEqual(registry.get('setup_call_cleanup', 3), null, 'setup_call_cleanup/3 is absent from the ISO registry');
|
|
6145
6164
|
assertEqual(Boolean(library.get('setup_call_cleanup', 3)), true, 'setup_call_cleanup/3 is an EyeProlog cleanup control');
|
|
6146
|
-
assertEqual(registeredNativeEyePrologLibraryNames().length,
|
|
6165
|
+
assertEqual(registeredNativeEyePrologLibraryNames().length, 58, 'public host-supported EyeProlog library count');
|
|
6147
6166
|
assertEqual(eyePrologPortableLibraryIndicators.length, 241, 'portable Prolog library count');
|
|
6148
|
-
assertEqual(eyePrologInteropLibraryIndicators.length,
|
|
6167
|
+
assertEqual(eyePrologInteropLibraryIndicators.length, 189, 'cross-implementation interop profile count');
|
|
6149
6168
|
assertEqual(eyePrologInteropLibraryModules.length, 23, 'common explicit library module profile count');
|
|
6150
6169
|
assertEqual(eyePrologInteropAutoload['member/2'], 'lists', 'member/2 canonical autoload');
|
|
6151
6170
|
assertEqual(eyePrologInteropAutoload['between/3'], 'between', 'between/3 canonical internal autoload');
|
|
@@ -6154,9 +6173,10 @@ answer(ok) :-
|
|
|
6154
6173
|
assertEqual(eyePrologInteropAutoload['time/1'], 'iso_ext', 'time/1 canonical interop autoload');
|
|
6155
6174
|
assertEqual(eyePrologInteropAutoload['.../2'], 'iso_ext', '.../2 canonical interop autoload');
|
|
6156
6175
|
assertEqual(eyePrologInteropAutoload['set_nth0/4'] ?? null, null, 'EyeProlog-only set_nth0/4 is not autoloadable');
|
|
6157
|
-
assertEqual(eyePrologNativeLibraryIndicators.length,
|
|
6176
|
+
assertEqual(eyePrologNativeLibraryIndicators.length, 58, 'host-supported library count');
|
|
6158
6177
|
assertEqual(eyePrologNativeLibraryIndicators.slice(0, 3).join(','), 'call_nth/2,freeze/2,dif/2', 'control and constraint predicates requiring host support');
|
|
6159
|
-
assertEqual(
|
|
6178
|
+
assertEqual(eyePrologNativeLibraryIndicators.includes('random/1'), true, 'stateful random/1 is classified as host-supported');
|
|
6179
|
+
assertEqual(eyePrologLibraryIndicators.length, 299, 'complete non-ISO EyeProlog library surface');
|
|
6160
6180
|
assertEqual(registry.get('eyeprolog__call_nth', 2), null, 'private call_nth adapter is absent from ISO registry');
|
|
6161
6181
|
assertEqual(Boolean(library.get('eyeprolog__call_nth', 2)), true, 'private call_nth adapter is registered for EyeProlog');
|
|
6162
6182
|
assertEqual(Boolean(library.get('eyeprolog__call_residue_vars', 2)), true, 'private call_residue_vars adapter is registered for EyeProlog');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -6237,12 +6237,12 @@ so side effects occur in Prolog execution order.
|
|
|
6237
6237
|
|
|
6238
6238
|
### The EyeProlog library
|
|
6239
6239
|
|
|
6240
|
-
EyeProlog exposes **
|
|
6240
|
+
EyeProlog exposes **299 distinct non-ISO library and normal-extension predicate
|
|
6241
6241
|
indicators** in addition to the 129 indicators in its isolated ISO profile.
|
|
6242
6242
|
**241 are defined entirely as ordinary Prolog clauses** in focused modules under
|
|
6243
|
-
`src/lib/`; **
|
|
6243
|
+
`src/lib/`; **58 use host support** for control, attributed variables,
|
|
6244
6244
|
constraints, or observability. The ISO and library catalogs therefore cover
|
|
6245
|
-
**
|
|
6245
|
+
**428 distinct predicate indicators**. Normal-mode controls and observability
|
|
6246
6246
|
relations that do not belong to a library, such as `call_cleanup/2`,
|
|
6247
6247
|
`setup_call_cleanup/3`, `statistics/0,2`, and `tnot/1`, are documented in their
|
|
6248
6248
|
own sections rather than counted as library predicates. `time/1` is additionally
|
|
@@ -6337,7 +6337,7 @@ bindings make one aligned subterm pair sufficient. For example:
|
|
|
6337
6337
|
| `library(pio)` | `phrase_from_file/2`, `phrase_from_file/3`, `phrase_to_file/2`, `phrase_to_file/3`, `phrase_to_stream/2` | Eager portable DCG file and stream I/O |
|
|
6338
6338
|
| `library(primes)` | `smallest_divisor_from/3` | Prime factor support |
|
|
6339
6339
|
| `library(prologue)` | `member/2`, `append/3`, `length/2`, `between/3`, `select/3`, `succ/2`, `maplist/2`, `maplist/3`, `maplist/4`, `maplist/5`, `maplist/6`, `maplist/7`, `maplist/8`, `nth0/3`, `nth0/4`, `nth1/3`, `nth1/4`, `call_nth/2`, `freeze/2`, `foldl/4`, `foldl/5`, `foldl/6`, `countall/2` | Legacy facade over canonical focused modules |
|
|
6340
|
-
| `library(random)` | `maybe/0`, `random/3`, `random_integer/3`, `set_random/1` | Common mutable-seed interface plus EyeProlog's pure state-threaded generator |
|
|
6340
|
+
| `library(random)` | `maybe/0`, `random/1`, `random/3`, `random_integer/3`, `set_random/1` | Common mutable-seed interface plus EyeProlog's pure state-threaded generator |
|
|
6341
6341
|
| `library(reif)` | `,/3`, `;/3`, `=/3`, `cond_t/3`, `dif/3`, `if_/3`, `memberd_t/3`, `tfilter/3`, `tmember/2`, `tmember_t/3`, `tpartition/4` | Reified conditions and list filtering; reused upstream source |
|
|
6342
6342
|
| `library(si)` | `atom_si/1`, `integer_si/1`, `atomic_si/1`, `list_si/1`, `character_si/1`, `term_si/1`, `chars_si/1`, `dif_si/2`, `not_si/1`, `when_si/2` | Sufficient-instantiation checks used by CLP(Z) |
|
|
6343
6343
|
| `library(strings)` | `matches/3`, `split/3`, `replace/4`, `lowercase/2`, `uppercase/2`, `trim/2`, `number_string/2`, `atom_string/2`, `term_string/2`, `string_concat/3`, `contains/2`, `matches/2`, `join/3`, `substring/4` | Text relations |
|
|
@@ -6558,6 +6558,12 @@ is a compatible declaration rather than a second tabling engine.
|
|
|
6558
6558
|
`format_time//2` supports the documented year, month, day, time, month-name,
|
|
6559
6559
|
weekday-name, and day-of-year specifiers.
|
|
6560
6560
|
|
|
6561
|
+
`random/1` keeps the common mutable-seed interface but uses a private native
|
|
6562
|
+
state step for hot-loop performance; its public sequence remains the same
|
|
6563
|
+
Park-Miller sequence used by the portable explicit-state `random/3`. The helper
|
|
6564
|
+
is internal to the normal EyeProlog registry and is not part of the ISO or
|
|
6565
|
+
public library predicate catalogs.
|
|
6566
|
+
|
|
6561
6567
|
`uuidv4/1`, `uuidv4_string/1`, and `uuid_string/2` provide the common UUID byte
|
|
6562
6568
|
list and character-list interface. `uuid(+Seed0,-UUID,-Seed)` remains an
|
|
6563
6569
|
EyeProlog extension that creates a version 4 UUID atom using pure `random/3`.
|
|
@@ -6605,6 +6611,7 @@ before calling the corresponding predicate.
|
|
|
6605
6611
|
| Predicates and principal modes | Behavior |
|
|
6606
6612
|
| --- | --- |
|
|
6607
6613
|
| `lt(+A,+B)`, `le(+A,+B)`, `gt(+A,+B)`, `ge(+A,+B)` | Compare integers exactly, finite numeric text numerically, `PnYnMnD` duration text component-wise, and other lexical values by string order. These differ from ISO arithmetic comparison and standard term order. |
|
|
6614
|
+
| `random(-Value)` | Stateful Park-Miller step using the library seed set by `set_random/1`; implemented through a private native fast path while retaining the same sequence and numeric conversion as `random/3`. |
|
|
6608
6615
|
| `random(+Seed0,-Value,-Seed)` | Portable Park-Miller generator with explicit state. `Value` is in `[0,1)`; pass the returned integer `Seed` to the next call. The same initial integer seed always reproduces the same sequence. |
|
|
6609
6616
|
| `difference(+End,+Start,-Duration)` | Portable Prolog. Computes a nonnegative calendar difference between ISO date atoms/character lists and returns atom `'PnYnMnD'`. Invalid dates or an end before the start fail. |
|
|
6610
6617
|
|