eyeprolog 1.2.23 → 1.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/program.js +36 -11
- package/src/solver.js +168 -5
- package/test/run-regression.mjs +49 -3
- package/the-art-of-eyeprolog.md +13 -2
package/package.json
CHANGED
package/src/program.js
CHANGED
|
@@ -159,6 +159,9 @@ export class Program {
|
|
|
159
159
|
scalarFactsOnly: true,
|
|
160
160
|
dynamic: this.dynamicPredicates.has(modulePredicateKey(module, name, arity)),
|
|
161
161
|
negationStratum: null,
|
|
162
|
+
hasCut: false,
|
|
163
|
+
cutReachable: null,
|
|
164
|
+
bundledLibrary: false,
|
|
162
165
|
};
|
|
163
166
|
return group;
|
|
164
167
|
}
|
|
@@ -186,6 +189,7 @@ export class Program {
|
|
|
186
189
|
group.rejectedDemandIndexes.clear();
|
|
187
190
|
}
|
|
188
191
|
group.clauses.push(clause);
|
|
192
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
189
193
|
const clausePosition = group.clauses.length - 1;
|
|
190
194
|
for (let i = 0; i < head.arity; i++) indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
|
|
191
195
|
}
|
|
@@ -292,6 +296,7 @@ export class Program {
|
|
|
292
296
|
const groups = [...this.groups.values()];
|
|
293
297
|
const indexByGroup = new Map(groups.map((group, i) => [group, i]));
|
|
294
298
|
const deps = groups.map(() => new Set());
|
|
299
|
+
const cutDeps = groups.map(() => new Set());
|
|
295
300
|
const negativeEdges = [];
|
|
296
301
|
for (const group of groups) {
|
|
297
302
|
const groupIndex = indexByGroup.get(group);
|
|
@@ -299,11 +304,18 @@ export class Program {
|
|
|
299
304
|
if (isCompactBinaryClause(clause)) {
|
|
300
305
|
if (clause.bodyName != null) {
|
|
301
306
|
const dep = this.findGroup(clause.bodyName, 2, group.module);
|
|
302
|
-
if (dep)
|
|
307
|
+
if (dep) {
|
|
308
|
+
deps[groupIndex].add(indexByGroup.get(dep));
|
|
309
|
+
cutDeps[groupIndex].add(indexByGroup.get(dep));
|
|
310
|
+
}
|
|
303
311
|
}
|
|
304
312
|
continue;
|
|
305
313
|
}
|
|
306
314
|
for (const goal of clause.body) {
|
|
315
|
+
for (const dependency of collectGoalDependencies(goal, false, true)) {
|
|
316
|
+
const dep = this.findGroup(dependency.name, dependency.arity, dependency.module ?? group.module);
|
|
317
|
+
if (dep) cutDeps[groupIndex].add(indexByGroup.get(dep));
|
|
318
|
+
}
|
|
307
319
|
const directKey = directGoalDependencyKey(goal);
|
|
308
320
|
if (directKey) {
|
|
309
321
|
const dep = this.findGroup(goal.name, goal.arity, goal.module ?? group.module);
|
|
@@ -325,6 +337,8 @@ export class Program {
|
|
|
325
337
|
const start = indexByGroup.get(group);
|
|
326
338
|
const standardLibraryModule = group.module !== 'user' &&
|
|
327
339
|
this.modules.get(group.module)?.filename?.startsWith('src/lib/');
|
|
340
|
+
group.bundledLibrary = standardLibraryModule === true;
|
|
341
|
+
group.cutReachable = [...reachableIndexes(start, cutDeps)].some((index) => groups[index].hasCut);
|
|
328
342
|
const seen = new Set();
|
|
329
343
|
const stack = [start];
|
|
330
344
|
let recursive = false;
|
|
@@ -533,6 +547,7 @@ class ProgramBuilder {
|
|
|
533
547
|
group.clauses.push(clause);
|
|
534
548
|
clause.groundHead = termHasNoVariables(head);
|
|
535
549
|
clause.scalarHead = head.type === COMPOUND && head.args.every(isScalar);
|
|
550
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
536
551
|
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
537
552
|
for (let i = 0; i < head.arity; i++) {
|
|
538
553
|
indexOne(group.argIndexes[i], head.args[i], clause, group.clauses, clausePosition);
|
|
@@ -1114,33 +1129,41 @@ function directGoalDependencyKey(goal) {
|
|
|
1114
1129
|
return `${goal.name}/${goal.arity}`;
|
|
1115
1130
|
}
|
|
1116
1131
|
|
|
1117
|
-
function collectGoalDependencies(goal, negated) {
|
|
1132
|
+
function collectGoalDependencies(goal, negated, traverseConditionals = false) {
|
|
1118
1133
|
if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated }];
|
|
1119
1134
|
if (goal.type !== COMPOUND) return [];
|
|
1120
1135
|
if (goal.name === ',' && goal.arity === 2) {
|
|
1121
1136
|
return [
|
|
1122
|
-
...collectGoalDependencies(goal.args[0], negated),
|
|
1123
|
-
...collectGoalDependencies(goal.args[1], negated),
|
|
1137
|
+
...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
|
|
1138
|
+
...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
|
|
1139
|
+
];
|
|
1140
|
+
}
|
|
1141
|
+
if (traverseConditionals && (goal.name === ';' || goal.name === '->') && goal.arity === 2) {
|
|
1142
|
+
return [
|
|
1143
|
+
...collectGoalDependencies(goal.args[0], negated, true),
|
|
1144
|
+
...collectGoalDependencies(goal.args[1], negated, true),
|
|
1124
1145
|
];
|
|
1125
1146
|
}
|
|
1126
1147
|
if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) {
|
|
1127
|
-
return collectGoalDependencies(goal.args[0], !negated);
|
|
1148
|
+
return collectGoalDependencies(goal.args[0], !negated, traverseConditionals);
|
|
1128
1149
|
}
|
|
1129
1150
|
if (goal.name === 'once' && goal.arity === 1) {
|
|
1130
|
-
return collectGoalDependencies(goal.args[0], negated);
|
|
1151
|
+
return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
|
|
1131
1152
|
}
|
|
1132
1153
|
if (goal.name === 'forall' && goal.arity === 2) {
|
|
1133
1154
|
return [
|
|
1134
|
-
...collectGoalDependencies(goal.args[0], negated),
|
|
1135
|
-
...collectGoalDependencies(goal.args[1], negated),
|
|
1155
|
+
...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
|
|
1156
|
+
...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
|
|
1136
1157
|
];
|
|
1137
1158
|
}
|
|
1138
1159
|
if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
|
|
1139
|
-
return collectGoalDependencies(goal.args[1], negated);
|
|
1160
|
+
return collectGoalDependencies(goal.args[1], negated, traverseConditionals);
|
|
1161
|
+
}
|
|
1162
|
+
if (goal.name === 'countall' && goal.arity === 2) {
|
|
1163
|
+
return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
|
|
1140
1164
|
}
|
|
1141
|
-
if (goal.name === 'countall' && goal.arity === 2) return collectGoalDependencies(goal.args[0], negated);
|
|
1142
1165
|
if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
|
|
1143
|
-
return collectGoalDependencies(goal.args[2], negated);
|
|
1166
|
+
return collectGoalDependencies(goal.args[2], negated, traverseConditionals);
|
|
1144
1167
|
}
|
|
1145
1168
|
return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated }];
|
|
1146
1169
|
}
|
|
@@ -1308,6 +1331,7 @@ function rebuildGroupIndexes(group) {
|
|
|
1308
1331
|
group.demandIndexes.clear();
|
|
1309
1332
|
group.rejectedDemandIndexes.clear();
|
|
1310
1333
|
group.scalarFactsOnly = true;
|
|
1334
|
+
group.hasCut = false;
|
|
1311
1335
|
for (let clausePosition = 0; clausePosition < group.clauses.length; clausePosition++) {
|
|
1312
1336
|
const clause = group.clauses[clausePosition];
|
|
1313
1337
|
if (isCompactBinaryClause(clause)) {
|
|
@@ -1320,6 +1344,7 @@ function rebuildGroupIndexes(group) {
|
|
|
1320
1344
|
}
|
|
1321
1345
|
clause.groundHead = termHasNoVariables(clause.head);
|
|
1322
1346
|
clause.scalarHead = clause.head.type === COMPOUND && clause.head.args.every(isScalar);
|
|
1347
|
+
if (clauseHasCut(clause)) group.hasCut = true;
|
|
1323
1348
|
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
1324
1349
|
for (let i = 0; i < group.arity; i++) indexOne(group.argIndexes[i], clause.head.args[i], clause, group.clauses, clausePosition);
|
|
1325
1350
|
}
|
package/src/solver.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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, Env, compound, copyResolved, deref, emptyList,
|
|
5
|
-
|
|
4
|
+
COMPOUND, NUMBER, VAR, Env, compound, cons, copyResolved, deref, emptyList,
|
|
5
|
+
flattenConjunction, freshTerm, isCons, isDecimalInteger, isEmptyList,
|
|
6
|
+
numberTerm, numberTextFromDouble, termIsGround, termToString, unify, variable, variantTerms,
|
|
6
7
|
} from './term.js';
|
|
7
8
|
import { sameNumberValue } from './number-value.js';
|
|
8
9
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
@@ -367,6 +368,23 @@ export class Solver {
|
|
|
367
368
|
}
|
|
368
369
|
qualifyMetaArguments(goal, group);
|
|
369
370
|
|
|
371
|
+
const lengthIterator = prologueLengthIterator(this, group, goal, env);
|
|
372
|
+
if (lengthIterator != null) {
|
|
373
|
+
const firstResult = lengthIterator.next();
|
|
374
|
+
if (firstResult.done) break;
|
|
375
|
+
stack.push({
|
|
376
|
+
kind: 'resumeBuiltin',
|
|
377
|
+
iterator: lengthIterator,
|
|
378
|
+
goals: rest,
|
|
379
|
+
depth: depth + 1,
|
|
380
|
+
active,
|
|
381
|
+
});
|
|
382
|
+
goals = rest;
|
|
383
|
+
env = firstResult.value;
|
|
384
|
+
depth++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
370
388
|
if (group.tabled) {
|
|
371
389
|
const key = memoKey(goal, env, group);
|
|
372
390
|
if (key.hasBound) {
|
|
@@ -418,8 +436,8 @@ export class Solver {
|
|
|
418
436
|
return activeVariantIn(goal, env, this.active);
|
|
419
437
|
}
|
|
420
438
|
|
|
421
|
-
checkMemoryLimit() {
|
|
422
|
-
if (this.inferences < this.nextMemoryCheck) return;
|
|
439
|
+
checkMemoryLimit(force = false) {
|
|
440
|
+
if (!force && this.inferences < this.nextMemoryCheck) return;
|
|
423
441
|
this.nextMemoryCheck = this.inferences + 256;
|
|
424
442
|
if (!Number.isFinite(this.maxMemoryBytes)) return;
|
|
425
443
|
const used = usedHeapSize();
|
|
@@ -477,6 +495,12 @@ export class Solver {
|
|
|
477
495
|
if (!unify(goal, freshHead, next)) continue;
|
|
478
496
|
if (freshBody.length === 0) {
|
|
479
497
|
yield* this.solve(rest, next, depth + 1);
|
|
498
|
+
} else if (!groupNeedsActiveFrame(group)) {
|
|
499
|
+
for (const bodyEnv of this.solve(freshBody, next, depth + 1)) {
|
|
500
|
+
if (this.solutionsSeen > 0) this.solutionsSeen--;
|
|
501
|
+
yield* this.solve(rest, bodyEnv, depth + 1);
|
|
502
|
+
if (this.solutionsSeen >= this.solutionLimit) break;
|
|
503
|
+
}
|
|
480
504
|
} else {
|
|
481
505
|
yield* this.solveRuleBodyThenRest(goal, env, freshBody, rest, next, depth);
|
|
482
506
|
}
|
|
@@ -613,7 +637,11 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
613
637
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
614
638
|
const frames = [];
|
|
615
639
|
const invocation = { goal, env };
|
|
616
|
-
|
|
640
|
+
// Active frames serve two purposes: they delimit cut and detect variants in
|
|
641
|
+
// recursive user predicates. Cut-free, non-recursive library helpers need
|
|
642
|
+
// neither. Copying their full active stack at every recursive step made
|
|
643
|
+
// otherwise linear relations such as length/2 retain O(depth^2) references.
|
|
644
|
+
const guarded = groupNeedsActiveFrame(group);
|
|
617
645
|
const release = guarded ? [{ kind: 'releaseActive' }] : [];
|
|
618
646
|
const nextActive = guarded ? [...active, invocation] : active;
|
|
619
647
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
@@ -663,6 +691,141 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
663
691
|
for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
|
|
664
692
|
}
|
|
665
693
|
|
|
694
|
+
function groupNeedsActiveFrame(group) {
|
|
695
|
+
// User code may observe the surrounding control context through later cuts,
|
|
696
|
+
// so only apply this planning shortcut to the fixed bundled-library graph.
|
|
697
|
+
if (group.bundledLibrary !== true) return true;
|
|
698
|
+
// A frame is also required above a cut-bearing callee. The disjunction
|
|
699
|
+
// builtin uses the caller marker to distinguish a callee-local cut from a
|
|
700
|
+
// cut in its own branch. null means dependency analysis was intentionally
|
|
701
|
+
// disabled (strict mode or a newly mutated group), so remain conservative.
|
|
702
|
+
return group.cutReachable !== false || (group.recursive && !group.linearNumeric);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function prologueLengthIterator(solver, group, goal, env) {
|
|
706
|
+
if (solver.registry.eyePrologLibrary !== true ||
|
|
707
|
+
group.module !== 'prologue' || group.name !== 'length' || group.arity !== 2 ||
|
|
708
|
+
group.bundledLibrary !== true || group.clauses.length !== 2) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Delayed and constrained variables need the ordinary solver's wake-up
|
|
713
|
+
// points. The fast path is deliberately limited to plain finite-tree terms.
|
|
714
|
+
if (env._clpz != null) return null;
|
|
715
|
+
const length = deref(goal.args[1], env);
|
|
716
|
+
if (length.type === VAR && env._delays?.has(length.name)) return null;
|
|
717
|
+
|
|
718
|
+
let cursor = deref(goal.args[0], env);
|
|
719
|
+
while (isCons(cursor)) {
|
|
720
|
+
cursor = deref(cursor.args[1], env);
|
|
721
|
+
}
|
|
722
|
+
if (cursor.type === VAR) {
|
|
723
|
+
if (env._delays?.has(cursor.name)) return null;
|
|
724
|
+
if (length.type === VAR && cursor.name === length.name) return null;
|
|
725
|
+
}
|
|
726
|
+
return prologueLengthSolutions(solver, goal, env);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function* prologueLengthSolutions(solver, goal, env) {
|
|
730
|
+
const requestedLength = deref(goal.args[1], env);
|
|
731
|
+
if (requestedLength.type !== VAR) {
|
|
732
|
+
if (requestedLength.type !== NUMBER || !isDecimalInteger(requestedLength.name)) {
|
|
733
|
+
throw new PrologError('type_error(integer)', requestedLength);
|
|
734
|
+
}
|
|
735
|
+
const length = BigInt(requestedLength.name);
|
|
736
|
+
if (length < 0n) throw new PrologError('domain_error(not_less_than_zero)', requestedLength);
|
|
737
|
+
yield* fixedLengthSolutions(solver, goal.args[0], length, env);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
yield* generatedLengthSolutions(solver, goal.args[0], goal.args[1], env);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function* fixedLengthSolutions(solver, list, length, env) {
|
|
745
|
+
let cursor = deref(list, env);
|
|
746
|
+
let remaining = length;
|
|
747
|
+
let steps = 0n;
|
|
748
|
+
while (isCons(cursor)) {
|
|
749
|
+
if (remaining === 0n) return;
|
|
750
|
+
remaining--;
|
|
751
|
+
cursor = deref(cursor.args[1], env);
|
|
752
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
753
|
+
}
|
|
754
|
+
if (isEmptyList(cursor)) {
|
|
755
|
+
if (remaining === 0n) yield env;
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (cursor.type !== VAR) return;
|
|
759
|
+
|
|
760
|
+
// A source-level anonymous variable occurs nowhere else, so materializing
|
|
761
|
+
// its list cannot affect any subsequent goal or answer substitution.
|
|
762
|
+
if (isAnonymousVariable(cursor)) {
|
|
763
|
+
yield env;
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const id = nextFreshId();
|
|
768
|
+
let suffix = emptyList();
|
|
769
|
+
for (let index = 0n; index < remaining; index++) {
|
|
770
|
+
suffix = cons(variable(`__length${id}_${index}`), suffix);
|
|
771
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
772
|
+
}
|
|
773
|
+
const next = env.clone();
|
|
774
|
+
solver.stats.unify_calls++;
|
|
775
|
+
if (unify(cursor, suffix, next)) yield next;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function* generatedLengthSolutions(solver, list, length, env) {
|
|
779
|
+
let cursor = deref(list, env);
|
|
780
|
+
let count = 0n;
|
|
781
|
+
let steps = 0n;
|
|
782
|
+
while (isCons(cursor)) {
|
|
783
|
+
count++;
|
|
784
|
+
cursor = deref(cursor.args[1], env);
|
|
785
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
786
|
+
}
|
|
787
|
+
if (isEmptyList(cursor)) {
|
|
788
|
+
const next = bindGeneratedLength(solver, length, count, env);
|
|
789
|
+
if (next != null) yield next;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (cursor.type !== VAR) return;
|
|
793
|
+
|
|
794
|
+
if (isAnonymousVariable(cursor)) {
|
|
795
|
+
for (let value = count; ; value++) {
|
|
796
|
+
const next = bindGeneratedLength(solver, length, value, env);
|
|
797
|
+
if (next != null) yield next;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const id = nextFreshId();
|
|
802
|
+
let suffix = emptyList();
|
|
803
|
+
for (let extra = 0n; ; extra++) {
|
|
804
|
+
const next = env.clone();
|
|
805
|
+
solver.stats.unify_calls++;
|
|
806
|
+
if (unify(cursor, suffix, next)) {
|
|
807
|
+
const answer = bindGeneratedLength(solver, length, count + extra, next);
|
|
808
|
+
if (answer != null) yield answer;
|
|
809
|
+
}
|
|
810
|
+
suffix = cons(variable(`__length${id}_${extra}`), suffix);
|
|
811
|
+
lengthAllocationCheckpoint(solver, ++steps);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function bindGeneratedLength(solver, length, value, env) {
|
|
816
|
+
const next = env.clone();
|
|
817
|
+
solver.stats.unify_calls++;
|
|
818
|
+
return unify(length, numberTerm(value), next) ? next : null;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function isAnonymousVariable(term) {
|
|
822
|
+
return term.type === VAR && term.name.startsWith('__anon');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function lengthAllocationCheckpoint(solver, steps) {
|
|
826
|
+
if ((steps & 255n) === 0n) solver.checkMemoryLimit(true);
|
|
827
|
+
}
|
|
828
|
+
|
|
666
829
|
function pushFastPiFrames(stack, goal, rest, env, depth, active) {
|
|
667
830
|
const values = goal.args.map((arg) => deref(arg, env));
|
|
668
831
|
if ([0, 1, 2, 4].some((index) => values[index].type !== 'number')) return false;
|
package/test/run-regression.mjs
CHANGED
|
@@ -681,6 +681,21 @@ c4 ?- call((!;1)).
|
|
|
681
681
|
assertEqual(result.stderr, '', 'quad stderr');
|
|
682
682
|
},
|
|
683
683
|
},
|
|
684
|
+
{
|
|
685
|
+
name: 'REPL advances anonymous Prologue length checks through I = 28',
|
|
686
|
+
run: () => {
|
|
687
|
+
const result = runCli([], {
|
|
688
|
+
input:
|
|
689
|
+
'use_module(library(prologue)).\n' +
|
|
690
|
+
'length(_,I),I>9,N is 2^I,\\+ \\+ length(_,N).\n' +
|
|
691
|
+
'f\nf\nf\n;\n;\n;\n;\n\nhalt.\n',
|
|
692
|
+
});
|
|
693
|
+
assertEqual(result.status, 0, 'exit status');
|
|
694
|
+
assertIncludes(result.stdout, '; I = 28, N = 268435456\n; ... .\n?- ', 'large anonymous length answer');
|
|
695
|
+
assertNotIncludes(result.stdout, 'resource_error(memory)', 'stdout');
|
|
696
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
697
|
+
},
|
|
698
|
+
},
|
|
684
699
|
{
|
|
685
700
|
name: 'Prologue freeze wakes delayed goals with their bindings',
|
|
686
701
|
run: () => {
|
|
@@ -2472,14 +2487,14 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2472
2487
|
},
|
|
2473
2488
|
},
|
|
2474
2489
|
{
|
|
2475
|
-
name: 'list allocation heap pressure becomes resource_error(memory)',
|
|
2490
|
+
name: 'named list allocation heap pressure becomes resource_error(memory)',
|
|
2476
2491
|
run: () => {
|
|
2477
2492
|
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2478
2493
|
const programText = ':- use_module(library(prologue)).\n';
|
|
2479
|
-
const goalText = '
|
|
2494
|
+
const goalText = '\\+ \\+ length(List, 1000000)';
|
|
2480
2495
|
const script = `
|
|
2481
2496
|
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2482
|
-
const program = Program.parse(${JSON.stringify(programText)});
|
|
2497
|
+
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
2483
2498
|
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2484
2499
|
const goal = parseGoalText(${JSON.stringify(goalText)}, {
|
|
2485
2500
|
operatorDefinitions: [...program.operators.values()],
|
|
@@ -2500,6 +2515,37 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2500
2515
|
assertEqual(result.stdout, 'resource_error(memory)', 'heap pressure resource error');
|
|
2501
2516
|
},
|
|
2502
2517
|
},
|
|
2518
|
+
{
|
|
2519
|
+
name: 'anonymous Prologue length checks avoid materializing discarded lists',
|
|
2520
|
+
run: () => {
|
|
2521
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2522
|
+
const programText = ':- use_module(library(prologue)).\n';
|
|
2523
|
+
const goalText = 'length(_, I), I > 9, N is 2^I, \\+ \\+ length(_, N)';
|
|
2524
|
+
const script = `
|
|
2525
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2526
|
+
const program = Program.parse(${JSON.stringify(programText)}, { sourceMetadata: false });
|
|
2527
|
+
const solver = new Solver(program, {
|
|
2528
|
+
registry: getEyePrologRegistry(),
|
|
2529
|
+
solutionLimit: 19,
|
|
2530
|
+
});
|
|
2531
|
+
const goal = parseGoalText(${JSON.stringify(goalText)}, {
|
|
2532
|
+
operatorDefinitions: [...program.operators.values()],
|
|
2533
|
+
});
|
|
2534
|
+
let answers = 0;
|
|
2535
|
+
for (const _ of solver.solve([goal], new Env(), 0)) answers++;
|
|
2536
|
+
process.stdout.write(String(answers));
|
|
2537
|
+
`;
|
|
2538
|
+
const result = spawnSync(process.execPath, [
|
|
2539
|
+
'--max-old-space-size=64',
|
|
2540
|
+
'--input-type=module',
|
|
2541
|
+
'--eval',
|
|
2542
|
+
script,
|
|
2543
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2544
|
+
if (result.error) throw result.error;
|
|
2545
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2546
|
+
assertEqual(result.stdout, '19', 'answers through I = 28');
|
|
2547
|
+
},
|
|
2548
|
+
},
|
|
2503
2549
|
{
|
|
2504
2550
|
name: 'solver honors solution limits',
|
|
2505
2551
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1843,6 +1843,17 @@ ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
|
|
|
1843
1843
|
`finite_memory` spelling for the distinct convention where no finite amount of
|
|
1844
1844
|
memory could complete the computation.
|
|
1845
1845
|
|
|
1846
|
+
The iterative solver keeps active-call frames only where they are semantically
|
|
1847
|
+
needed for cut scope or recursive variant guards. Bundled-library helpers whose
|
|
1848
|
+
callable dependency region is cut-free and which need no recursive variant
|
|
1849
|
+
guard therefore do not copy a growing active-call sequence at every step.
|
|
1850
|
+
Under the normal EyeProlog registry, the bundled Prologue `length/2` also has a
|
|
1851
|
+
scoped iterative execution path: named lists are counted or constructed without
|
|
1852
|
+
recursive interpreter frames, and an anonymous list is not materialized because
|
|
1853
|
+
its binding cannot be observed. The ordinary clauses remain the authoritative
|
|
1854
|
+
module definition and are used unchanged by the ISO-only registry and whenever
|
|
1855
|
+
delays or finite-domain constraints require their normal wake-up points.
|
|
1856
|
+
|
|
1846
1857
|
### Implementation boundary
|
|
1847
1858
|
|
|
1848
1859
|
The source layout mirrors the language boundary. `src/iso.js` contains the
|
|
@@ -5918,8 +5929,8 @@ so side effects occur in Prolog execution order.
|
|
|
5918
5929
|
### The EyeProlog library
|
|
5919
5930
|
|
|
5920
5931
|
EyeProlog exposes **99 library predicate indicators** in addition to the 129
|
|
5921
|
-
indicators in its isolated ISO profile. **60 are
|
|
5922
|
-
|
|
5932
|
+
indicators in its isolated ISO profile. **60 are defined as ordinary Prolog
|
|
5933
|
+
clauses** in focused modules under `src/lib/`. The remaining 39
|
|
5923
5934
|
are public wrappers around backtrackable host support: Prologue `call_nth/2` and
|
|
5924
5935
|
`freeze/2`, plus the 37-predicate finite-domain `library(clpz)` kernel. The
|
|
5925
5936
|
resulting normal EyeProlog language surface is therefore **228 public predicate
|