eyeprolog 1.2.32 → 1.2.34
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 +16 -0
- package/package.json +1 -1
- package/src/lib/clpz.pl +35 -33
- package/src/platform.js +9 -4
- package/src/program.js +24 -0
- package/src/solver.js +156 -55
- package/src/term.js +104 -16
- package/test/run-regression.mjs +99 -4
package/index.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ export interface EyePrologPredicateGroup {
|
|
|
93
93
|
rejectedDemandIndexes: Set<string>;
|
|
94
94
|
tabled: boolean;
|
|
95
95
|
recursive: boolean;
|
|
96
|
+
listTailRecursive: boolean;
|
|
96
97
|
tableInputPositions: number[];
|
|
97
98
|
negationStratum: number | null;
|
|
98
99
|
}
|
|
@@ -107,6 +108,14 @@ export class Term {
|
|
|
107
108
|
get arity(): number;
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
export class CompactListTerm {
|
|
112
|
+
readonly type: 'compound';
|
|
113
|
+
readonly name: '.';
|
|
114
|
+
readonly args: EyePrologTerm[];
|
|
115
|
+
readonly arity: 2;
|
|
116
|
+
mayContainVariable(name: string): boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
export class Env {
|
|
111
120
|
constructor(bindings?: Iterable<readonly [string, EyePrologTerm]> | null);
|
|
112
121
|
bindings: Map<string, EyePrologTerm>;
|
|
@@ -198,6 +207,9 @@ export function numberTerm(value: string | number): Term;
|
|
|
198
207
|
export function compound(name: string, args?: EyePrologTerm[]): Term;
|
|
199
208
|
export function emptyList(): Term;
|
|
200
209
|
export function cons(head: EyePrologTerm, tail: EyePrologTerm): Term;
|
|
210
|
+
export function compactVariableList(length: bigint | number | string, variablePrefix: string): CompactListTerm | Term;
|
|
211
|
+
export function isCompactList(term: EyePrologTerm | null | undefined): term is CompactListTerm;
|
|
212
|
+
export function compactListLength(term: EyePrologTerm | null | undefined): bigint | null;
|
|
201
213
|
export function deref(term: EyePrologTerm, env: Env): EyePrologTerm;
|
|
202
214
|
export function isScalar(term: EyePrologTerm | null | undefined): boolean;
|
|
203
215
|
export function isEmptyList(term: EyePrologTerm | null | undefined): boolean;
|
|
@@ -259,6 +271,7 @@ declare const eyeprolog: {
|
|
|
259
271
|
NUMBER: typeof NUMBER;
|
|
260
272
|
COMPOUND: typeof COMPOUND;
|
|
261
273
|
Term: typeof Term;
|
|
274
|
+
CompactListTerm: typeof CompactListTerm;
|
|
262
275
|
Env: typeof Env;
|
|
263
276
|
Program: typeof Program;
|
|
264
277
|
Solver: typeof Solver;
|
|
@@ -273,6 +286,9 @@ declare const eyeprolog: {
|
|
|
273
286
|
compound: typeof compound;
|
|
274
287
|
emptyList: typeof emptyList;
|
|
275
288
|
cons: typeof cons;
|
|
289
|
+
compactVariableList: typeof compactVariableList;
|
|
290
|
+
isCompactList: typeof isCompactList;
|
|
291
|
+
compactListLength: typeof compactListLength;
|
|
276
292
|
deref: typeof deref;
|
|
277
293
|
isScalar: typeof isScalar;
|
|
278
294
|
isEmptyList: typeof isEmptyList;
|
package/package.json
CHANGED
package/src/lib/clpz.pl
CHANGED
|
@@ -7,22 +7,24 @@
|
|
|
7
7
|
constraints will be added as their propagation semantics become available.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
% Current operator names in predicate indicators are parenthesized as bare
|
|
11
|
+
% atoms (for example, (#>)/2); quoting is unnecessary.
|
|
10
12
|
:- module(clpz, [
|
|
11
|
-
(
|
|
12
|
-
(
|
|
13
|
-
(
|
|
14
|
-
(
|
|
15
|
-
(
|
|
16
|
-
(
|
|
17
|
-
(
|
|
18
|
-
(
|
|
19
|
-
(
|
|
20
|
-
(
|
|
21
|
-
(
|
|
22
|
-
(
|
|
23
|
-
(
|
|
24
|
-
(
|
|
25
|
-
(
|
|
13
|
+
(#>)/2,
|
|
14
|
+
(#<)/2,
|
|
15
|
+
(#>=)/2,
|
|
16
|
+
(#=<)/2,
|
|
17
|
+
(#=)/2,
|
|
18
|
+
(#\=)/2,
|
|
19
|
+
(#\)/1,
|
|
20
|
+
(#<==>)/2,
|
|
21
|
+
(#==>)/2,
|
|
22
|
+
(#<==)/2,
|
|
23
|
+
(#\/)/2,
|
|
24
|
+
(#\)/2,
|
|
25
|
+
(#/\)/2,
|
|
26
|
+
(in)/2,
|
|
27
|
+
(ins)/2,
|
|
26
28
|
all_different/1,
|
|
27
29
|
all_distinct/1,
|
|
28
30
|
nvalue/2,
|
|
@@ -50,33 +52,33 @@
|
|
|
50
52
|
:- op(760, yfx, #<==>).
|
|
51
53
|
:- op(750, xfy, #==>).
|
|
52
54
|
:- op(750, yfx, #<==).
|
|
53
|
-
:- op(740, yfx,
|
|
54
|
-
:- op(730, yfx,
|
|
55
|
-
:- op(720, yfx,
|
|
56
|
-
:- op(710, fy,
|
|
55
|
+
:- op(740, yfx, #\/).
|
|
56
|
+
:- op(730, yfx, #\).
|
|
57
|
+
:- op(720, yfx, #/\).
|
|
58
|
+
:- op(710, fy, #\).
|
|
57
59
|
:- op(700, xfx, #>).
|
|
58
60
|
:- op(700, xfx, #<).
|
|
59
61
|
:- op(700, xfx, #>=).
|
|
60
62
|
:- op(700, xfx, #=<).
|
|
61
63
|
:- op(700, xfx, #=).
|
|
62
|
-
:- op(700, xfx,
|
|
64
|
+
:- op(700, xfx, #\=).
|
|
63
65
|
:- op(700, xfx, in).
|
|
64
66
|
:- op(700, xfx, ins).
|
|
65
67
|
:- op(450, xfx, ..).
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
#>(Left, Right) :- eyeprolog__clpz_post(#>(Left, Right)).
|
|
70
|
+
#<(Left, Right) :- eyeprolog__clpz_post(#<(Left, Right)).
|
|
71
|
+
#>=(Left, Right) :- eyeprolog__clpz_post(#>=(Left, Right)).
|
|
72
|
+
#=<(Left, Right) :- eyeprolog__clpz_post(#=<(Left, Right)).
|
|
73
|
+
#=(Left, Right) :- eyeprolog__clpz_post(#=(Left, Right)).
|
|
74
|
+
#\=(Left, Right) :- eyeprolog__clpz_post(#\=(Left, Right)).
|
|
75
|
+
#\(Constraint) :- eyeprolog__clpz_post(#\(Constraint)).
|
|
76
|
+
#<==>(Left, Right) :- eyeprolog__clpz_post(#<==>(Left, Right)).
|
|
77
|
+
#==>(Left, Right) :- eyeprolog__clpz_post(#==>(Left, Right)).
|
|
78
|
+
#<==(Left, Right) :- eyeprolog__clpz_post(#<==(Left, Right)).
|
|
79
|
+
#\/(Left, Right) :- eyeprolog__clpz_post(#\/(Left, Right)).
|
|
80
|
+
#\(Left, Right) :- eyeprolog__clpz_post(#\(Left, Right)).
|
|
81
|
+
#/\(Left, Right) :- eyeprolog__clpz_post(#/\(Left, Right)).
|
|
80
82
|
|
|
81
83
|
in(Integer, Domain) :- eyeprolog__clpz_in(Integer, Domain).
|
|
82
84
|
ins(Integers, Domain) :- eyeprolog__clpz_ins(Integers, Domain).
|
package/src/platform.js
CHANGED
|
@@ -43,6 +43,14 @@ export function usedHeapSize() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function softHeapLimit() {
|
|
46
|
+
const limit = hardHeapLimit();
|
|
47
|
+
// Leave ample room for the generator stack to unwind and for the top level
|
|
48
|
+
// to construct and print resource_error(memory). Fatal V8 OOMs cannot be
|
|
49
|
+
// caught after the heap limit itself has been reached.
|
|
50
|
+
return Number.isFinite(limit) && limit > 0 ? Math.floor(limit * 0.75) : Infinity;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hardHeapLimit() {
|
|
46
54
|
let limit = null;
|
|
47
55
|
if (isNode) {
|
|
48
56
|
limit = v8?.getHeapStatistics?.().heap_size_limit ?? null;
|
|
@@ -51,10 +59,7 @@ export function softHeapLimit() {
|
|
|
51
59
|
const memory = globalThis.performance?.memory;
|
|
52
60
|
if (Number.isFinite(memory?.jsHeapSizeLimit)) limit = memory.jsHeapSizeLimit;
|
|
53
61
|
}
|
|
54
|
-
|
|
55
|
-
// to construct and print resource_error(memory). Fatal V8 OOMs cannot be
|
|
56
|
-
// caught after the heap limit itself has been reached.
|
|
57
|
-
return Number.isFinite(limit) && limit > 0 ? Math.floor(limit * 0.75) : Infinity;
|
|
62
|
+
return Number.isFinite(limit) && limit > 0 ? limit : Infinity;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
function configuredOldSpaceBytes() {
|
package/src/program.js
CHANGED
|
@@ -368,6 +368,11 @@ export class Program {
|
|
|
368
368
|
(isPiAccumulator(group) || isPortableBetweenGenerator(group));
|
|
369
369
|
group.linearNumeric = linearNumeric;
|
|
370
370
|
group.fastPi = linearNumeric && isPiAccumulator(group);
|
|
371
|
+
const directRecursiveComponent = plannedRecursive && [...deps[start]].every((dependency) =>
|
|
372
|
+
dependency === start || !reachableIndexes(dependency, deps).has(start)
|
|
373
|
+
);
|
|
374
|
+
group.listTailRecursive = directRecursiveComponent && !group.cutRecursive &&
|
|
375
|
+
hasStrictListTailRecursion(group);
|
|
371
376
|
group.tabled = plannedRecursive &&
|
|
372
377
|
!componentHasNegativeEdge(start, deps, negativeEdges) &&
|
|
373
378
|
!group.cutRecursive &&
|
|
@@ -1063,6 +1068,25 @@ function inferStructuralInputPositions(group) {
|
|
|
1063
1068
|
return Array.from({ length: group.arity }, (_, index) => index);
|
|
1064
1069
|
}
|
|
1065
1070
|
|
|
1071
|
+
function hasStrictListTailRecursion(group) {
|
|
1072
|
+
let foundRecursiveCall = false;
|
|
1073
|
+
for (const clause of group.clauses) {
|
|
1074
|
+
if (isCompactBinaryClause(clause)) return false;
|
|
1075
|
+
for (const goal of clause.body) {
|
|
1076
|
+
if (goal.type !== COMPOUND || goal.name !== group.name || goal.arity !== group.arity) continue;
|
|
1077
|
+
foundRecursiveCall = true;
|
|
1078
|
+
const decreases = goal.args.some((argument, index) => {
|
|
1079
|
+
const head = clause.head.args[index];
|
|
1080
|
+
return head?.type === COMPOUND && head.name === '.' && head.arity === 2 &&
|
|
1081
|
+
head.args[1]?.type === VAR && argument?.type === VAR &&
|
|
1082
|
+
head.args[1].name === argument.name;
|
|
1083
|
+
});
|
|
1084
|
+
if (!decreases) return false;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return foundRecursiveCall;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1066
1090
|
function hasLinearNumericRecursion(group) {
|
|
1067
1091
|
let recursiveClause = null;
|
|
1068
1092
|
for (const clause of group.clauses) {
|
package/src/solver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Depth-first EyeProlog solver with builtin dispatch, memoization, and guarded recursion handling.
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
3
|
import {
|
|
4
|
-
COMPOUND, NUMBER, VAR, Env, compound, cons, copyResolved, deref, emptyList,
|
|
4
|
+
COMPOUND, NUMBER, VAR, Env, compactListLength, compactVariableList, compound, cons, copyResolved, deref, emptyList,
|
|
5
5
|
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
6
|
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
|
|
7
7
|
} from './term.js';
|
|
@@ -11,7 +11,7 @@ import { getEyePrologRegistry } from './standard-library.js';
|
|
|
11
11
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
12
12
|
import { StreamManager } from './io.js';
|
|
13
13
|
import { clpzStateConsistent } from './clpz.js';
|
|
14
|
-
import { softHeapLimit, usedHeapSize } from './platform.js';
|
|
14
|
+
import { hardHeapLimit, softHeapLimit, usedHeapSize } from './platform.js';
|
|
15
15
|
|
|
16
16
|
let freshCounter = 0;
|
|
17
17
|
|
|
@@ -51,6 +51,11 @@ export class Solver {
|
|
|
51
51
|
this.inferences = 0;
|
|
52
52
|
this.inferenceLimitExceeded = false;
|
|
53
53
|
this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
|
|
54
|
+
this.memoryRecovery = options.memoryRecovery ?? {
|
|
55
|
+
active: false,
|
|
56
|
+
reservationBytes: 0,
|
|
57
|
+
checks: 0,
|
|
58
|
+
};
|
|
54
59
|
this.nextMemoryCheck = 0;
|
|
55
60
|
// Do not impose an implicit answer cap. Infinite and very large searches are
|
|
56
61
|
// part of normal Prolog semantics; callers that need a resource bound can
|
|
@@ -121,6 +126,7 @@ export class Solver {
|
|
|
121
126
|
maxDepth: this.maxDepth,
|
|
122
127
|
maxInferences: this.maxInferences,
|
|
123
128
|
maxMemoryBytes: this.maxMemoryBytes,
|
|
129
|
+
memoryRecovery: this.memoryRecovery,
|
|
124
130
|
solutionLimit,
|
|
125
131
|
isoStrict: this.isoStrict,
|
|
126
132
|
prologFlags: this.prologFlags,
|
|
@@ -424,7 +430,18 @@ export class Solver {
|
|
|
424
430
|
}
|
|
425
431
|
}
|
|
426
432
|
} catch (error) {
|
|
427
|
-
|
|
433
|
+
const normalized = normalizeHostResourceError(error);
|
|
434
|
+
if (normalized instanceof PrologError && normalized.formal === 'resource_error(memory)') {
|
|
435
|
+
// Unwinding makes query-local terms unreachable, but hosts are free to
|
|
436
|
+
// postpone collection. Give the shared solver family bounded breathing
|
|
437
|
+
// room on its next query so a GC can observe those released references.
|
|
438
|
+
if (!this.memoryRecovery.active) {
|
|
439
|
+
this.memoryRecovery.active = true;
|
|
440
|
+
this.memoryRecovery.reservationBytes = 1024 * 1024;
|
|
441
|
+
this.memoryRecovery.checks = 16;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
throw normalized;
|
|
428
445
|
} finally {
|
|
429
446
|
const stackIndex = this.solveStacks.indexOf(registeredStack);
|
|
430
447
|
if (stackIndex >= 0) this.solveStacks.splice(stackIndex, 1);
|
|
@@ -441,7 +458,12 @@ export class Solver {
|
|
|
441
458
|
this.nextMemoryCheck = this.inferences + 256;
|
|
442
459
|
if (!Number.isFinite(this.maxMemoryBytes)) return;
|
|
443
460
|
const used = usedHeapSize();
|
|
444
|
-
if (used != null && used
|
|
461
|
+
if (used != null && used < this.maxMemoryBytes) this.finishMemoryRecovery();
|
|
462
|
+
if (used != null && used >= this.currentMemoryLimit()) {
|
|
463
|
+
if (this.memoryRecovery.active && this.memoryRecovery.checks > 0) {
|
|
464
|
+
this.memoryRecovery.checks--;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
445
467
|
throw new PrologError('resource_error(memory)');
|
|
446
468
|
}
|
|
447
469
|
}
|
|
@@ -449,11 +471,32 @@ export class Solver {
|
|
|
449
471
|
checkMemoryReservation(bytes) {
|
|
450
472
|
if (!Number.isFinite(this.maxMemoryBytes) || !Number.isFinite(bytes) || bytes <= 0) return;
|
|
451
473
|
const used = usedHeapSize();
|
|
452
|
-
if (used != null &&
|
|
474
|
+
if (used != null && used < this.maxMemoryBytes) this.finishMemoryRecovery();
|
|
475
|
+
if (used != null && bytes > Math.max(0, this.currentMemoryLimit() - used)) {
|
|
476
|
+
if (this.memoryRecovery.active && bytes <= this.memoryRecovery.reservationBytes) {
|
|
477
|
+
this.memoryRecovery.reservationBytes -= bytes;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
453
480
|
throw new PrologError('resource_error(memory)');
|
|
454
481
|
}
|
|
455
482
|
}
|
|
456
483
|
|
|
484
|
+
currentMemoryLimit() {
|
|
485
|
+
if (!this.memoryRecovery.active) return this.maxMemoryBytes;
|
|
486
|
+
// Retain at least five percent of the actual host ceiling for error
|
|
487
|
+
// construction and unwinding. For an embedder-supplied lower soft limit,
|
|
488
|
+
// cap the temporary recovery window as well.
|
|
489
|
+
const hostSafetyLimit = hardHeapLimit() * 0.95;
|
|
490
|
+
const recoveryAllowance = Math.max(8 * 1024 * 1024, this.maxMemoryBytes * 0.125);
|
|
491
|
+
return Math.min(hostSafetyLimit, this.maxMemoryBytes + recoveryAllowance);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
finishMemoryRecovery() {
|
|
495
|
+
this.memoryRecovery.active = false;
|
|
496
|
+
this.memoryRecovery.reservationBytes = 0;
|
|
497
|
+
this.memoryRecovery.checks = 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
457
500
|
*solveUserGoal(goal, rest, env, depth) {
|
|
458
501
|
this.stats.solve_one_goal_calls++;
|
|
459
502
|
if (depth > this.maxDepth) {
|
|
@@ -700,6 +743,10 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
700
743
|
}
|
|
701
744
|
|
|
702
745
|
function groupNeedsActiveFrame(group) {
|
|
746
|
+
// A direct recursive call that consumes the tail of a matched list cannot
|
|
747
|
+
// revisit an earlier finite-tree call. It needs neither a cycle guard nor an
|
|
748
|
+
// O(depth) copy of the active-call stack at every element.
|
|
749
|
+
if (group.listTailRecursive === true && group.hasCut !== true) return false;
|
|
703
750
|
// User code may observe the surrounding control context through later cuts,
|
|
704
751
|
// so only apply this planning shortcut to the fixed bundled-library graph.
|
|
705
752
|
if (group.bundledLibrary !== true) return true;
|
|
@@ -772,23 +819,21 @@ function* fixedLengthSolutions(solver, list, length, env) {
|
|
|
772
819
|
return;
|
|
773
820
|
}
|
|
774
821
|
|
|
775
|
-
// A materialized list cell with its fresh variable occupies roughly 216
|
|
776
|
-
// bytes on current V8 builds. Reserve conservatively before constructing a
|
|
777
|
-
// huge list so a resource_error does not leave the heap at the soft limit
|
|
778
|
-
// and poison the next query.
|
|
779
|
-
const estimatedListCellBytes = 256n;
|
|
780
822
|
if (remaining > BigInt(Number.MAX_SAFE_INTEGER)) throw new PrologError('resource_error(memory)');
|
|
781
|
-
solver.checkMemoryReservation(Number(remaining * estimatedListCellBytes));
|
|
782
|
-
|
|
783
823
|
const id = nextFreshId();
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
824
|
+
// Keep an unobserved fixed-length list as one compact skeleton. Unification,
|
|
825
|
+
// list predicates, and readback expand it one cell at a time if its elements
|
|
826
|
+
// are actually inspected.
|
|
827
|
+
solver.checkMemoryReservation(256);
|
|
828
|
+
const suffix = compactVariableList(remaining, `__length${id}_`);
|
|
789
829
|
const next = env.clone();
|
|
790
830
|
solver.stats.unify_calls++;
|
|
791
|
-
|
|
831
|
+
// cursor is dereferenced and the compact skeleton contains only freshly
|
|
832
|
+
// generated variables, so this binding cannot create a cycle. Binding it
|
|
833
|
+
// directly avoids traversing and expanding the new skeleton for an occurs
|
|
834
|
+
// check whose result is known by construction.
|
|
835
|
+
next.bind(cursor.name, suffix);
|
|
836
|
+
yield next;
|
|
792
837
|
}
|
|
793
838
|
|
|
794
839
|
function* generatedLengthSolutions(solver, list, length, env) {
|
|
@@ -1338,18 +1383,61 @@ function variantShape(term, env) {
|
|
|
1338
1383
|
return term.args.map((arg) => variantArgumentSize(arg, env)).join(',');
|
|
1339
1384
|
}
|
|
1340
1385
|
|
|
1386
|
+
const rawProperListLengths = new WeakMap();
|
|
1387
|
+
|
|
1388
|
+
function rawProperListLength(term) {
|
|
1389
|
+
const compactLength = compactListLength(term);
|
|
1390
|
+
if (compactLength != null) return compactLength;
|
|
1391
|
+
if (!isCons(term)) return null;
|
|
1392
|
+
const cells = [];
|
|
1393
|
+
const seen = new WeakSet();
|
|
1394
|
+
let cursor = term;
|
|
1395
|
+
let suffixLength = 0;
|
|
1396
|
+
while (isCons(cursor)) {
|
|
1397
|
+
const cached = rawProperListLengths.get(cursor);
|
|
1398
|
+
if (cached != null) {
|
|
1399
|
+
suffixLength = cached;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
if (seen.has(cursor)) return null;
|
|
1403
|
+
seen.add(cursor);
|
|
1404
|
+
cells.push(cursor);
|
|
1405
|
+
// Only cache the raw spine. A variable tail may resolve differently in
|
|
1406
|
+
// separate environments and therefore needs the general shape walk.
|
|
1407
|
+
cursor = cursor.args[1];
|
|
1408
|
+
}
|
|
1409
|
+
if (!isEmptyList(cursor) && rawProperListLengths.get(cursor) == null) return null;
|
|
1410
|
+
for (let index = cells.length - 1; index >= 0; index--) {
|
|
1411
|
+
rawProperListLengths.set(cells[index], ++suffixLength);
|
|
1412
|
+
}
|
|
1413
|
+
return rawProperListLengths.get(term) ?? suffixLength;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1341
1416
|
function variantArgumentSize(term, env) {
|
|
1342
|
-
const
|
|
1417
|
+
const resolved = derefForLocal(term, env);
|
|
1418
|
+
const listLength = rawProperListLength(resolved);
|
|
1419
|
+
if (listLength != null) return `list:${listLength}`;
|
|
1420
|
+
const pending = [{ term, exit: false }];
|
|
1421
|
+
const ancestors = new WeakSet();
|
|
1343
1422
|
let size = 0;
|
|
1344
1423
|
while (pending.length > 0) {
|
|
1345
|
-
const
|
|
1424
|
+
const item = pending.pop();
|
|
1425
|
+
if (item.exit) {
|
|
1426
|
+
ancestors.delete(item.term);
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
const current = derefForLocal(item.term, env);
|
|
1346
1430
|
size++;
|
|
1347
|
-
// This is only a rejection key. Capping keeps pathological cyclic or very
|
|
1348
|
-
// large terms bounded; equal capped sizes still fall through to the exact
|
|
1349
|
-
// variant check.
|
|
1350
|
-
if (size > 4096) return 4097;
|
|
1351
1431
|
if (current?.type === COMPOUND) {
|
|
1352
|
-
|
|
1432
|
+
// Finite terms get an exact size, which cheaply distinguishes successive
|
|
1433
|
+
// tails of a long list. Keep cyclic terms conservative so the exact
|
|
1434
|
+
// variant check remains authoritative.
|
|
1435
|
+
if (ancestors.has(current)) return '*';
|
|
1436
|
+
ancestors.add(current);
|
|
1437
|
+
pending.push({ term: current, exit: true });
|
|
1438
|
+
for (let index = current.arity - 1; index >= 0; index--) {
|
|
1439
|
+
pending.push({ term: current.args[index], exit: false });
|
|
1440
|
+
}
|
|
1353
1441
|
}
|
|
1354
1442
|
}
|
|
1355
1443
|
return size;
|
|
@@ -1400,45 +1488,58 @@ function derefForLocal(term, env) {
|
|
|
1400
1488
|
}
|
|
1401
1489
|
|
|
1402
1490
|
function memoKey(goal, env, group = null) {
|
|
1403
|
-
let hasBound = false;
|
|
1404
|
-
const variables = new Map();
|
|
1405
1491
|
const required = group?.tableInputPositions ?? [];
|
|
1406
|
-
const ground =
|
|
1492
|
+
const ground = goal.args.map((arg) => termIsGround(arg, env));
|
|
1493
|
+
const hasBound = required.length > 0
|
|
1494
|
+
? required.some((index) => ground[index])
|
|
1495
|
+
: ground.some(Boolean);
|
|
1496
|
+
// Automatic tabling only admits calls with a ground input. Avoid building a
|
|
1497
|
+
// potentially huge canonical key for the non-ground structural calls that
|
|
1498
|
+
// will use the ordinary active-call guard instead.
|
|
1499
|
+
if (!hasBound) return { hasBound: false, text: '' };
|
|
1500
|
+
|
|
1501
|
+
const variables = new Map();
|
|
1407
1502
|
const parts = goal.args.map((arg) => {
|
|
1408
1503
|
const value = derefForLocal(arg, env);
|
|
1409
|
-
|
|
1410
|
-
ground.push(false);
|
|
1411
|
-
return '_';
|
|
1412
|
-
}
|
|
1413
|
-
const canonical = canonicalTermInfo(value, env, variables);
|
|
1414
|
-
ground.push(canonical.ground);
|
|
1415
|
-
if (canonical.ground) hasBound = true;
|
|
1416
|
-
return canonical.key;
|
|
1504
|
+
return value.type === 'var' ? '_' : canonicalTermKey(value, env, variables);
|
|
1417
1505
|
});
|
|
1418
|
-
if (required.length > 0) {
|
|
1419
|
-
hasBound = required.some((index) => ground[index]);
|
|
1420
|
-
}
|
|
1421
1506
|
return { hasBound, text: parts.join('|') };
|
|
1422
1507
|
}
|
|
1423
1508
|
|
|
1424
|
-
function
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1509
|
+
function canonicalTermKey(term, env, variables) {
|
|
1510
|
+
// Memo keys are needed before deciding whether a recursive call is tabled.
|
|
1511
|
+
// Build them iteratively: a perfectly ordinary bound list can be deeper than
|
|
1512
|
+
// JavaScript's native call stack even though the solver itself is iterative.
|
|
1513
|
+
const key = [];
|
|
1514
|
+
const pending = [{ kind: 'term', term }];
|
|
1515
|
+
while (pending.length > 0) {
|
|
1516
|
+
const item = pending.pop();
|
|
1517
|
+
if (item.kind === 'text') {
|
|
1518
|
+
key.push(item.text);
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
const value = derefForLocal(item.term, env);
|
|
1522
|
+
if (value.type === 'var') {
|
|
1523
|
+
let id = variables.get(value.name);
|
|
1524
|
+
if (id == null) {
|
|
1525
|
+
id = variables.size;
|
|
1526
|
+
variables.set(value.name, id);
|
|
1527
|
+
}
|
|
1528
|
+
key.push(`var:${id}`);
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (!value.args?.length) {
|
|
1532
|
+
key.push(`${value.type}:${value.name}`);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
key.push(`${value.type}:${value.name}(`);
|
|
1536
|
+
pending.push({ kind: 'text', text: ')' });
|
|
1537
|
+
for (let index = value.args.length - 1; index >= 0; index--) {
|
|
1538
|
+
if (index < value.args.length - 1) pending.push({ kind: 'text', text: ',' });
|
|
1539
|
+
pending.push({ kind: 'term', term: value.args[index] });
|
|
1431
1540
|
}
|
|
1432
|
-
return { key: `var:${id}`, ground: false };
|
|
1433
1541
|
}
|
|
1434
|
-
|
|
1435
|
-
let ground = true;
|
|
1436
|
-
const keys = value.args.map((arg) => {
|
|
1437
|
-
const child = canonicalTermInfo(arg, env, variables);
|
|
1438
|
-
if (!child.ground) ground = false;
|
|
1439
|
-
return child.key;
|
|
1440
|
-
});
|
|
1441
|
-
return { key: `${value.type}:${value.name}(${keys.join(',')})`, ground };
|
|
1542
|
+
return key.join('');
|
|
1442
1543
|
}
|
|
1443
1544
|
|
|
1444
1545
|
function copyResolvedWithKey(term, env, variables) {
|
package/src/term.js
CHANGED
|
@@ -8,6 +8,7 @@ export const STRING = 'string';
|
|
|
8
8
|
export const NUMBER = 'number';
|
|
9
9
|
export const COMPOUND = 'compound';
|
|
10
10
|
const EMPTY_ARGS = Object.freeze([]);
|
|
11
|
+
const ENV_FLATTEN_DEPTH = 1024;
|
|
11
12
|
|
|
12
13
|
export class Term {
|
|
13
14
|
constructor(type, name, args = []) {
|
|
@@ -20,6 +21,55 @@ export class Term {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// A fixed-length list of fresh variables is represented as one compact
|
|
25
|
+
// skeleton and expanded cell-by-cell only when a goal actually inspects it.
|
|
26
|
+
// This keeps ordinary logical construction proportional to what the program
|
|
27
|
+
// observes instead of eagerly allocating two host objects per list element.
|
|
28
|
+
export class CompactListTerm {
|
|
29
|
+
constructor(length, variablePrefix, offset = 0n, state = null) {
|
|
30
|
+
this.type = COMPOUND;
|
|
31
|
+
this.name = '.';
|
|
32
|
+
this._compactLength = BigInt(length);
|
|
33
|
+
this._variablePrefix = variablePrefix;
|
|
34
|
+
this._offset = BigInt(offset);
|
|
35
|
+
this._compactState = state ?? { maxPossiblyBoundIndex: -1n };
|
|
36
|
+
this._args = null;
|
|
37
|
+
}
|
|
38
|
+
get arity() {
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
get args() {
|
|
42
|
+
if (this._args == null) {
|
|
43
|
+
const head = variable(`${this._variablePrefix}${this._offset}`);
|
|
44
|
+
head._compactState = this._compactState;
|
|
45
|
+
head._compactIndex = this._offset;
|
|
46
|
+
const tail = this._compactLength === 1n
|
|
47
|
+
? emptyList()
|
|
48
|
+
: new CompactListTerm(
|
|
49
|
+
this._compactLength - 1n,
|
|
50
|
+
this._variablePrefix,
|
|
51
|
+
this._offset + 1n,
|
|
52
|
+
this._compactState,
|
|
53
|
+
);
|
|
54
|
+
this._args = [head, tail];
|
|
55
|
+
}
|
|
56
|
+
return this._args;
|
|
57
|
+
}
|
|
58
|
+
mayContainVariable(name) {
|
|
59
|
+
if (String(name).startsWith(this._variablePrefix)) {
|
|
60
|
+
const indexText = String(name).slice(this._variablePrefix.length);
|
|
61
|
+
if (/^\d+$/.test(indexText)) {
|
|
62
|
+
const index = BigInt(indexText);
|
|
63
|
+
if (index >= this._offset && index < this._offset + this._compactLength) return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// If no generated head in this suffix has ever participated in a binding,
|
|
67
|
+
// an unrelated variable cannot occur below it. This remains conservative
|
|
68
|
+
// across backtracking because the high-water mark is never rolled back.
|
|
69
|
+
return this._compactState.maxPossiblyBoundIndex >= this._offset;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
23
73
|
export const variable = (name) => new Term(VAR, name, EMPTY_ARGS);
|
|
24
74
|
export const atom = (name) => new Term(ATOM, name, EMPTY_ARGS);
|
|
25
75
|
export const stringTerm = (value) => new Term(STRING, value, EMPTY_ARGS);
|
|
@@ -27,6 +77,12 @@ export const numberTerm = (value) => new Term(NUMBER, value, EMPTY_ARGS);
|
|
|
27
77
|
export const compound = (name, args = []) => args.length === 0 ? atom(name) : new Term(COMPOUND, name, args);
|
|
28
78
|
export const emptyList = () => atom('[]');
|
|
29
79
|
export const cons = (head, tail) => compound('.', [head, tail]);
|
|
80
|
+
export const compactVariableList = (length, variablePrefix) => {
|
|
81
|
+
const size = BigInt(length);
|
|
82
|
+
return size === 0n ? emptyList() : new CompactListTerm(size, variablePrefix);
|
|
83
|
+
};
|
|
84
|
+
export const isCompactList = (term) => term instanceof CompactListTerm;
|
|
85
|
+
export const compactListLength = (term) => isCompactList(term) ? term._compactLength : null;
|
|
30
86
|
|
|
31
87
|
export class Env {
|
|
32
88
|
constructor(bindings) {
|
|
@@ -94,7 +150,7 @@ export class Env {
|
|
|
94
150
|
return undefined;
|
|
95
151
|
}
|
|
96
152
|
bind(name, term) {
|
|
97
|
-
if (this._state.depth >=
|
|
153
|
+
if (this._state.depth >= ENV_FLATTEN_DEPTH) {
|
|
98
154
|
const flattened = new Map();
|
|
99
155
|
for (let state = this._state; state != null; state = state.parent) {
|
|
100
156
|
if (state.bindingName != null && !flattened.has(state.bindingName)) {
|
|
@@ -200,6 +256,7 @@ function occurs(variableName, term, env) {
|
|
|
200
256
|
if (binding !== undefined) stack.push(binding);
|
|
201
257
|
continue;
|
|
202
258
|
}
|
|
259
|
+
if (isCompactList(current) && !current.mayContainVariable(variableName)) continue;
|
|
203
260
|
if (current?.type !== COMPOUND || seenTerms.has(current)) continue;
|
|
204
261
|
seenTerms.add(current);
|
|
205
262
|
for (let i = 0; i < current.arity; i++) stack.push(current.args[i]);
|
|
@@ -224,6 +281,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
224
281
|
if (a.type === VAR && b.type === VAR) {
|
|
225
282
|
// Both variables are already dereferenced and unbound, so linking them
|
|
226
283
|
// cannot create a cycle and needs no occurs-check traversal.
|
|
284
|
+
markCompactVariableBound(a);
|
|
227
285
|
env.bind(a.name, b);
|
|
228
286
|
continue;
|
|
229
287
|
}
|
|
@@ -232,6 +290,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
232
290
|
occursCheckHandler?.(a, b, env);
|
|
233
291
|
return false;
|
|
234
292
|
}
|
|
293
|
+
markCompactVariableBound(a);
|
|
235
294
|
env.bind(a.name, b);
|
|
236
295
|
continue;
|
|
237
296
|
}
|
|
@@ -240,6 +299,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
240
299
|
occursCheckHandler?.(b, a, env);
|
|
241
300
|
return false;
|
|
242
301
|
}
|
|
302
|
+
markCompactVariableBound(b);
|
|
243
303
|
env.bind(b.name, a);
|
|
244
304
|
continue;
|
|
245
305
|
}
|
|
@@ -264,6 +324,13 @@ export function unify(left, right, env, options = {}) {
|
|
|
264
324
|
return true;
|
|
265
325
|
}
|
|
266
326
|
|
|
327
|
+
function markCompactVariableBound(term) {
|
|
328
|
+
if (term?._compactState == null || term._compactIndex == null) return;
|
|
329
|
+
if (term._compactIndex > term._compactState.maxPossiblyBoundIndex) {
|
|
330
|
+
term._compactState.maxPossiblyBoundIndex = term._compactIndex;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
267
334
|
export function cloneTerm(term) {
|
|
268
335
|
if (term.type === VAR) return variable(term.name);
|
|
269
336
|
const cloned = term.type === COMPOUND && term.arity === 0
|
|
@@ -307,7 +374,12 @@ export function termIsGround(term, env = new Env()) {
|
|
|
307
374
|
if (resolved.type === VAR) return false;
|
|
308
375
|
if (seen.has(resolved)) continue;
|
|
309
376
|
seen.add(resolved);
|
|
310
|
-
|
|
377
|
+
// Visit leftmost arguments first. Lists and other recursive structures
|
|
378
|
+
// commonly carry their first unbound variable there, allowing a
|
|
379
|
+
// non-ground check to finish without walking the complete tail.
|
|
380
|
+
for (let index = resolved.args.length - 1; index >= 0; index--) {
|
|
381
|
+
pending.push(resolved.args[index]);
|
|
382
|
+
}
|
|
311
383
|
}
|
|
312
384
|
return true;
|
|
313
385
|
}
|
|
@@ -505,21 +577,37 @@ export function termSignature(term) {
|
|
|
505
577
|
}
|
|
506
578
|
|
|
507
579
|
export function variantTerms(left, leftEnv, right, rightEnv, pairs = new Map(), reverse = new Map()) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
580
|
+
// Variant checks sit on the recursive-call hot path. Use an explicit work
|
|
581
|
+
// stack so long lists do not consume the JavaScript call stack.
|
|
582
|
+
const pending = [[left, right]];
|
|
583
|
+
const seen = new WeakMap();
|
|
584
|
+
while (pending.length > 0) {
|
|
585
|
+
[left, right] = pending.pop();
|
|
586
|
+
left = deref(left, leftEnv);
|
|
587
|
+
right = deref(right, rightEnv);
|
|
588
|
+
if (left.type === VAR || right.type === VAR) {
|
|
589
|
+
if (left.type !== VAR || right.type !== VAR) return false;
|
|
590
|
+
if (pairs.has(left.name) || reverse.has(right.name)) {
|
|
591
|
+
if (pairs.get(left.name) !== right.name || reverse.get(right.name) !== left.name) return false;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
pairs.set(left.name, right.name);
|
|
595
|
+
reverse.set(right.name, left.name);
|
|
596
|
+
continue;
|
|
514
597
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
598
|
+
|
|
599
|
+
if (left.type !== right.type || left.arity !== right.arity) return false;
|
|
600
|
+
if (left.type === NUMBER ? !sameNumberValue(left.name, right.name) : left.name !== right.name) return false;
|
|
601
|
+
if (left.type !== COMPOUND) continue;
|
|
602
|
+
|
|
603
|
+
let rights = seen.get(left);
|
|
604
|
+
if (rights?.has(right)) continue;
|
|
605
|
+
if (rights == null) {
|
|
606
|
+
rights = new WeakSet();
|
|
607
|
+
seen.set(left, rights);
|
|
608
|
+
}
|
|
609
|
+
rights.add(right);
|
|
610
|
+
for (let i = left.arity - 1; i >= 0; i--) pending.push([left.args[i], right.args[i]]);
|
|
523
611
|
}
|
|
524
612
|
return true;
|
|
525
613
|
}
|
package/test/run-regression.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
} from '../src/index.js';
|
|
41
41
|
import { parseGoalText, parseNumberTokenText } from '../src/parser.js';
|
|
42
42
|
import { compareTerms } from '../src/term.js';
|
|
43
|
+
import { formatTermForWrite } from '../src/write.js';
|
|
43
44
|
import { selectClauseCandidates } from '../src/program.js';
|
|
44
45
|
import { TestReporter, isMainModule } from './test-style.mjs';
|
|
45
46
|
import { buildConformanceReport, formatConformanceReport } from './run-conformance-report.mjs';
|
|
@@ -730,6 +731,36 @@ c4 ?- call((!;1)).
|
|
|
730
731
|
assertIncludes(caught.message, 'operator atom', 'syntax rejection');
|
|
731
732
|
},
|
|
732
733
|
},
|
|
734
|
+
{
|
|
735
|
+
name: 'CLP(Z) operator declarations avoid unnecessary quoted atoms',
|
|
736
|
+
run: () => {
|
|
737
|
+
const filename = path.join(packageRoot, 'src', 'lib', 'clpz.pl');
|
|
738
|
+
const source = fs.readFileSync(filename, 'utf8');
|
|
739
|
+
for (const name of ['#>', '#<', '#>=', '#=<', '#=', '#\\=', '#<==>', '#==>', '#<==', '#\\/', '#/\\']) {
|
|
740
|
+
assertIncludes(source, `(${name})/2`, `${name}/2 export is parenthesized`);
|
|
741
|
+
assertNotIncludes(source, `('${name}')/2`, `${name}/2 export is not quoted`);
|
|
742
|
+
assertNotIncludes(source, `'${name}'(`, `${name} functional notation is not quoted`);
|
|
743
|
+
}
|
|
744
|
+
assertIncludes(source, '(#\\)/1', '#\\/1 export is parenthesized');
|
|
745
|
+
assertIncludes(source, '(#\\)/2', '#\\/2 export is parenthesized');
|
|
746
|
+
assertIncludes(source, '(in)/2', 'in/2 export is parenthesized');
|
|
747
|
+
assertIncludes(source, '(ins)/2', 'ins/2 export is parenthesized');
|
|
748
|
+
assertNotIncludes(source, "('in')/2", 'in/2 export is not quoted');
|
|
749
|
+
assertNotIncludes(source, "('ins')/2", 'ins/2 export is not quoted');
|
|
750
|
+
|
|
751
|
+
const canonicalOptions = { quoted: true, ignoreOps: true, compact: true, operators: [] };
|
|
752
|
+
assertEqual(
|
|
753
|
+
formatTermForWrite(compound('#>', [atom('a'), atom('b')]), new Env(), canonicalOptions),
|
|
754
|
+
'#>(a,b)',
|
|
755
|
+
'canonical #> functor has no quotes',
|
|
756
|
+
);
|
|
757
|
+
assertEqual(
|
|
758
|
+
formatTermForWrite(compound('#\\=', [atom('a'), atom('b')]), new Env(), canonicalOptions),
|
|
759
|
+
'#\\=(a,b)',
|
|
760
|
+
'canonical #\\= functor has no quotes',
|
|
761
|
+
);
|
|
762
|
+
},
|
|
763
|
+
},
|
|
733
764
|
{
|
|
734
765
|
name: 'layout distinguishes prefix notation from functional notation',
|
|
735
766
|
run: () => {
|
|
@@ -2765,7 +2796,7 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2765
2796
|
},
|
|
2766
2797
|
},
|
|
2767
2798
|
{
|
|
2768
|
-
name: '
|
|
2799
|
+
name: 'discarded fixed-length lists stay compact under a bounded heap',
|
|
2769
2800
|
run: () => {
|
|
2770
2801
|
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2771
2802
|
const script = `
|
|
@@ -2776,11 +2807,75 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2776
2807
|
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
2777
2808
|
return [...solver.solve([goal], new Env(), 0)];
|
|
2778
2809
|
};
|
|
2810
|
+
execute('length(L,10000000),fail');
|
|
2811
|
+
if (execute('length(L,1),L=[X],X=L').length !== 0) {
|
|
2812
|
+
throw new Error('compact list admitted a cyclic binding');
|
|
2813
|
+
}
|
|
2779
2814
|
execute('length(L,1000),fail');
|
|
2815
|
+
process.stdout.write('compact');
|
|
2816
|
+
`;
|
|
2817
|
+
const result = spawnSync(process.execPath, [
|
|
2818
|
+
'--max-old-space-size=64',
|
|
2819
|
+
'--input-type=module',
|
|
2820
|
+
'--eval',
|
|
2821
|
+
script,
|
|
2822
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2823
|
+
if (result.error) throw result.error;
|
|
2824
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2825
|
+
assertEqual(result.stdout, 'compact', 'discarded list skeleton does not exhaust the heap');
|
|
2826
|
+
},
|
|
2827
|
+
},
|
|
2828
|
+
{
|
|
2829
|
+
name: 'deep recursive list guards do not consume the host call stack',
|
|
2830
|
+
run: () => {
|
|
2831
|
+
const program = Program.parse('l([]).\nl([_|L]) :- l(L).\n', { sourceMetadata: false });
|
|
2832
|
+
const left = listFromItems(Array.from({ length: 20000 }, (_, index) => variable(`L${index}`)));
|
|
2833
|
+
const right = listFromItems(Array.from({ length: 20000 }, (_, index) => variable(`R${index}`)));
|
|
2834
|
+
const solver = new Solver(program, {
|
|
2835
|
+
registry: getEyePrologRegistry(),
|
|
2836
|
+
maxDepth: 0,
|
|
2837
|
+
maxMemoryBytes: Infinity,
|
|
2838
|
+
});
|
|
2839
|
+
const answers = [...solver.solve([compound('l', [left])], new Env(), 0)];
|
|
2840
|
+
assertEqual(answers.length, 0, 'depth bound stops after stack-safe memo classification');
|
|
2841
|
+
assertEqual(variantTerms(left, new Env(), right, new Env()), true, 'deep lists are variants');
|
|
2842
|
+
},
|
|
2843
|
+
},
|
|
2844
|
+
{
|
|
2845
|
+
name: 'discarded recursive allocations recover after resource_error(memory)',
|
|
2846
|
+
run: () => {
|
|
2847
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2848
|
+
const programText = `
|
|
2849
|
+
:- use_module(library(prologue)).
|
|
2850
|
+
l([]).
|
|
2851
|
+
l([E|L]) :- length(E,1000), l(L).
|
|
2852
|
+
`;
|
|
2853
|
+
const reportedGoal = 'length(_,I),N is 2^I,\\+ \\+ (length(L,N),l(L)),L=[_|_]';
|
|
2854
|
+
const script = `
|
|
2855
|
+
import { Program, Solver, Env, deref, variable, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2856
|
+
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
2857
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2858
|
+
const execute = (text) => {
|
|
2859
|
+
const goal = parseGoalText(text, { operatorDefinitions: [...program.operators.values()] });
|
|
2860
|
+
return [...solver.solve([goal], new Env(), 0)];
|
|
2861
|
+
};
|
|
2862
|
+
const reported = parseGoalText(${JSON.stringify(reportedGoal)}, {
|
|
2863
|
+
operatorDefinitions: [...program.operators.values()],
|
|
2864
|
+
});
|
|
2865
|
+
let reportedAnswers = 0;
|
|
2866
|
+
for (const answer of solver.solve([reported], new Env(), 0)) {
|
|
2867
|
+
reportedAnswers++;
|
|
2868
|
+
if (reportedAnswers !== 14) continue;
|
|
2869
|
+
if (deref(variable('I'), answer).name !== '13' || deref(variable('N'), answer).name !== '8192') {
|
|
2870
|
+
throw new Error('reported query did not reach I=13, N=8192');
|
|
2871
|
+
}
|
|
2872
|
+
break;
|
|
2873
|
+
}
|
|
2874
|
+
if (reportedAnswers !== 14) throw new Error('reported query produced too few answers');
|
|
2780
2875
|
let caught = null;
|
|
2781
|
-
try { execute('length(L,
|
|
2876
|
+
try { execute('length(L,32768),l(L)'); } catch (error) { caught = error; }
|
|
2782
2877
|
if (caught?.formal !== 'resource_error(memory)') throw caught ?? new Error('no resource error');
|
|
2783
|
-
execute('length(L,
|
|
2878
|
+
if (execute('length(L,10),l(L)').length !== 1) throw new Error('recovery query failed');
|
|
2784
2879
|
process.stdout.write('recovered');
|
|
2785
2880
|
`;
|
|
2786
2881
|
const result = spawnSync(process.execPath, [
|
|
@@ -2791,7 +2886,7 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2791
2886
|
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2792
2887
|
if (result.error) throw result.error;
|
|
2793
2888
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2794
|
-
assertEqual(result.stdout, 'recovered', '
|
|
2889
|
+
assertEqual(result.stdout, 'recovered', 'discarded recursive terms are collectible after unwinding');
|
|
2795
2890
|
},
|
|
2796
2891
|
},
|
|
2797
2892
|
{
|