eyeprolog 1.3.8 → 1.3.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/README.md +3 -1
- package/package.json +1 -1
- package/src/program.js +51 -15
- package/src/solver.js +29 -6
- package/test/run-regression.mjs +56 -0
- package/the-art-of-eyeprolog.md +9 -3
package/README.md
CHANGED
|
@@ -208,7 +208,9 @@ Trealla/Scryer organization for predicates such as `member/2`, `memberchk/2`,
|
|
|
208
208
|
`append/2-3`, `nth0/3-4`, `nth1/3-4`, `length/2`, `maplist/2-8`, and
|
|
209
209
|
`foldl/4-6`. Its `length/2` remains relational: with both arguments variable,
|
|
210
210
|
`length(Xs, N)` enumerates lists of increasing length together with `N = 0, 1,
|
|
211
|
-
2, ...`.
|
|
211
|
+
2, ...`. Open-ended generation uses the normal memory guard with recovery
|
|
212
|
+
headroom, so an exhausted finite heap is reported as a catchable
|
|
213
|
+
`resource_error(memory)` instead of degenerating into quadratic list checks.
|
|
212
214
|
|
|
213
215
|
`library(iso_ext)` is also accepted as a common interop module name.
|
|
214
216
|
EyeProlog exports `call_nth/2` there, so Scryer-style source can explicitly use
|
package/package.json
CHANGED
package/src/program.js
CHANGED
|
@@ -314,15 +314,10 @@ export class Program {
|
|
|
314
314
|
const wfsNegativeEdges = [];
|
|
315
315
|
for (const group of groups) {
|
|
316
316
|
const groupIndex = indexByGroup.get(group);
|
|
317
|
+
let compactDependencies = null;
|
|
317
318
|
for (const clause of group.clauses) {
|
|
318
319
|
if (isCompactBinaryClause(clause)) {
|
|
319
|
-
if (clause.bodyName != null)
|
|
320
|
-
const dep = this.findGroup(clause.bodyName, 2, group.module);
|
|
321
|
-
if (dep) {
|
|
322
|
-
deps[groupIndex].add(indexByGroup.get(dep));
|
|
323
|
-
cutDeps[groupIndex].add(indexByGroup.get(dep));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
320
|
+
if (clause.bodyName != null) (compactDependencies ??= new Set()).add(clause.bodyName);
|
|
326
321
|
continue;
|
|
327
322
|
}
|
|
328
323
|
for (const goal of clause.body) {
|
|
@@ -349,6 +344,13 @@ export class Program {
|
|
|
349
344
|
}
|
|
350
345
|
}
|
|
351
346
|
}
|
|
347
|
+
for (const name of compactDependencies ?? []) {
|
|
348
|
+
const dep = this.findGroup(name, 2, group.module);
|
|
349
|
+
if (!dep) continue;
|
|
350
|
+
const dependencyIndex = indexByGroup.get(dep);
|
|
351
|
+
deps[groupIndex].add(dependencyIndex);
|
|
352
|
+
cutDeps[groupIndex].add(dependencyIndex);
|
|
353
|
+
}
|
|
352
354
|
}
|
|
353
355
|
const finiteDatalogCache = new Map();
|
|
354
356
|
const rangeRestrictedDatalogCache = new Map();
|
|
@@ -1301,12 +1303,10 @@ function datalogDependencyClauseCount(program, group, seen = new Set()) {
|
|
|
1301
1303
|
if (seen.has(group)) return 0;
|
|
1302
1304
|
seen.add(group);
|
|
1303
1305
|
let count = group.clauses.length;
|
|
1306
|
+
let compactDependencies = null;
|
|
1304
1307
|
for (const clause of group.clauses) {
|
|
1305
1308
|
if (isCompactBinaryClause(clause)) {
|
|
1306
|
-
if (clause.bodyName != null)
|
|
1307
|
-
const target = program.findGroup(clause.bodyName, 2, group.module);
|
|
1308
|
-
if (target) count += datalogDependencyClauseCount(program, target, seen);
|
|
1309
|
-
}
|
|
1309
|
+
if (clause.bodyName != null) (compactDependencies ??= new Set()).add(clause.bodyName);
|
|
1310
1310
|
continue;
|
|
1311
1311
|
}
|
|
1312
1312
|
for (const goal of clause.body) {
|
|
@@ -1315,6 +1315,10 @@ function datalogDependencyClauseCount(program, group, seen = new Set()) {
|
|
|
1315
1315
|
if (target) count += datalogDependencyClauseCount(program, target, seen);
|
|
1316
1316
|
}
|
|
1317
1317
|
}
|
|
1318
|
+
for (const name of compactDependencies ?? []) {
|
|
1319
|
+
const target = program.findGroup(name, 2, group.module);
|
|
1320
|
+
if (target) count += datalogDependencyClauseCount(program, target, seen);
|
|
1321
|
+
}
|
|
1318
1322
|
return count;
|
|
1319
1323
|
}
|
|
1320
1324
|
|
|
@@ -1324,12 +1328,10 @@ function isFiniteDatalogGroup(program, group, cache = new Map(), visiting = new
|
|
|
1324
1328
|
if (visiting.has(group)) return true;
|
|
1325
1329
|
visiting.add(group);
|
|
1326
1330
|
let finite = true;
|
|
1331
|
+
let compactDependencies = null;
|
|
1327
1332
|
for (const clause of group.clauses) {
|
|
1328
1333
|
if (isCompactBinaryClause(clause)) {
|
|
1329
|
-
if (clause.bodyName != null)
|
|
1330
|
-
const target = program.findGroup(clause.bodyName, 2, group.module);
|
|
1331
|
-
if (!target || !isFiniteDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1332
|
-
}
|
|
1334
|
+
if (clause.bodyName != null) (compactDependencies ??= new Set()).add(clause.bodyName);
|
|
1333
1335
|
continue;
|
|
1334
1336
|
}
|
|
1335
1337
|
if (clause.head.type === COMPOUND && !clause.head.args.every(isFiniteDatalogArgument)) { finite = false; break; }
|
|
@@ -1345,6 +1347,12 @@ function isFiniteDatalogGroup(program, group, cache = new Map(), visiting = new
|
|
|
1345
1347
|
}
|
|
1346
1348
|
if (!finite) break;
|
|
1347
1349
|
}
|
|
1350
|
+
if (finite) {
|
|
1351
|
+
for (const name of compactDependencies ?? []) {
|
|
1352
|
+
const target = program.findGroup(name, 2, group.module);
|
|
1353
|
+
if (!target || !isFiniteDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1348
1356
|
visiting.delete(group);
|
|
1349
1357
|
cache.set(group, finite);
|
|
1350
1358
|
return finite;
|
|
@@ -1358,7 +1366,35 @@ function isRangeRestrictedFiniteDatalogGroup(program, group, cache = new Map(),
|
|
|
1358
1366
|
visiting.add(group);
|
|
1359
1367
|
let finite = true;
|
|
1360
1368
|
|
|
1369
|
+
let compactDependencyResults = null;
|
|
1361
1370
|
for (const clause of group.clauses) {
|
|
1371
|
+
if (isCompactBinaryClause(clause)) {
|
|
1372
|
+
const head0Variable = clause.head0Type === VAR;
|
|
1373
|
+
const head1Variable = clause.head1Type === VAR;
|
|
1374
|
+
if (clause.bodyName == null) {
|
|
1375
|
+
if (head0Variable || head1Variable) { finite = false; break; }
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const head0RangeRestricted = !head0Variable ||
|
|
1379
|
+
(clause.body0Type === VAR && clause.body0Name === clause.head0Name) ||
|
|
1380
|
+
(clause.body1Type === VAR && clause.body1Name === clause.head0Name);
|
|
1381
|
+
const head1RangeRestricted = !head1Variable ||
|
|
1382
|
+
(clause.body0Type === VAR && clause.body0Name === clause.head1Name) ||
|
|
1383
|
+
(clause.body1Type === VAR && clause.body1Name === clause.head1Name);
|
|
1384
|
+
if (!head0RangeRestricted || !head1RangeRestricted) {
|
|
1385
|
+
finite = false;
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
let targetFinite = compactDependencyResults?.get(clause.bodyName);
|
|
1389
|
+
if (targetFinite == null) {
|
|
1390
|
+
const target = program.findGroup(clause.bodyName, 2, group.module);
|
|
1391
|
+
targetFinite = target != null && isRangeRestrictedFiniteDatalogGroup(program, target, cache, visiting);
|
|
1392
|
+
(compactDependencyResults ??= new Map()).set(clause.bodyName, targetFinite);
|
|
1393
|
+
}
|
|
1394
|
+
if (!targetFinite) { finite = false; break; }
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1362
1398
|
const head = clause.head;
|
|
1363
1399
|
if ((head.type !== COMPOUND && head.type !== ATOM) ||
|
|
1364
1400
|
(head.type === COMPOUND && !head.args.every(isFiniteDatalogArgument))) {
|
package/src/solver.js
CHANGED
|
@@ -17,6 +17,12 @@ import { evaluatePositiveDatalog, relationForDatalogGroup, datalogCandidateIndex
|
|
|
17
17
|
|
|
18
18
|
let freshCounter = 0;
|
|
19
19
|
const DEFAULT_INNER_TABLE_SCOPE_LIMIT = 1024;
|
|
20
|
+
// Conservative live-storage estimate for one generated length/2 list cell
|
|
21
|
+
// (cons object, argument vector, fresh variable, and its generated name).
|
|
22
|
+
const GENERATED_LENGTH_CELL_RESERVE_BYTES = 256;
|
|
23
|
+
const MAX_GENERATED_LENGTH_RESERVE_STEPS = BigInt(
|
|
24
|
+
Math.floor(Number.MAX_SAFE_INTEGER / GENERATED_LENGTH_CELL_RESERVE_BYTES),
|
|
25
|
+
);
|
|
20
26
|
|
|
21
27
|
function qualifyTerm(term, module) {
|
|
22
28
|
if (!term || (term.type !== COMPOUND && term.type !== 'atom')) return term;
|
|
@@ -1239,13 +1245,17 @@ function* generatedLengthSolutions(solver, list, length, env) {
|
|
|
1239
1245
|
let suffix = emptyList();
|
|
1240
1246
|
for (let extra = 0n; ; extra++) {
|
|
1241
1247
|
const next = env.clone();
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1248
|
+
// cursor is a dereferenced plain variable and suffix is made only from
|
|
1249
|
+
// freshly generated variables, so this binding cannot create a cycle.
|
|
1250
|
+
// Binding directly avoids an O(extra) occurs-check over the complete
|
|
1251
|
+
// growing suffix for every answer; without this, unbounded length/2
|
|
1252
|
+
// generation becomes quadratic and can spend hours before reaching the
|
|
1253
|
+
// normal memory resource guard (issue #49).
|
|
1254
|
+
next.bind(cursor.name, suffix);
|
|
1255
|
+
const answer = bindGeneratedLength(solver, length, count + extra, next);
|
|
1256
|
+
if (answer != null) yield answer;
|
|
1247
1257
|
suffix = cons(variable(`__length${id}_${extra}`), suffix);
|
|
1248
|
-
|
|
1258
|
+
generatedLengthAllocationCheckpoint(solver, extra + 1n);
|
|
1249
1259
|
}
|
|
1250
1260
|
}
|
|
1251
1261
|
|
|
@@ -1263,6 +1273,19 @@ function lengthAllocationCheckpoint(solver, steps) {
|
|
|
1263
1273
|
if ((steps & 255n) === 0n) solver.checkMemoryLimit(true);
|
|
1264
1274
|
}
|
|
1265
1275
|
|
|
1276
|
+
function generatedLengthAllocationCheckpoint(solver, steps) {
|
|
1277
|
+
if ((steps & 255n) !== 0n) return;
|
|
1278
|
+
// The open-ended generator retains its current list spine between answers.
|
|
1279
|
+
// Reserve room proportional to that live spine so the protected length/2
|
|
1280
|
+
// call raises resource_error(memory) before its caller's outer solver hits
|
|
1281
|
+
// the same heap limit. This makes the error catchable by catch/3.
|
|
1282
|
+
const estimatedSpineBytes = steps > MAX_GENERATED_LENGTH_RESERVE_STEPS
|
|
1283
|
+
? Number.MAX_SAFE_INTEGER
|
|
1284
|
+
: Number(steps) * GENERATED_LENGTH_CELL_RESERVE_BYTES;
|
|
1285
|
+
solver.checkMemoryReservation(estimatedSpineBytes);
|
|
1286
|
+
solver.checkMemoryLimit(true);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1266
1289
|
function pushFastPiFrames(stack, goal, rest, env, depth, active) {
|
|
1267
1290
|
const values = goal.args.map((arg) => deref(arg, env));
|
|
1268
1291
|
if ([0, 1, 2, 4].some((index) => values[index].type !== 'number')) return false;
|
package/test/run-regression.mjs
CHANGED
|
@@ -3627,6 +3627,45 @@ answer(ok) :-
|
|
|
3627
3627
|
}
|
|
3628
3628
|
},
|
|
3629
3629
|
},
|
|
3630
|
+
{
|
|
3631
|
+
name: 'unbounded length/2 reaches a catchable memory resource error (issue #49)',
|
|
3632
|
+
run: () => {
|
|
3633
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
3634
|
+
const script = `
|
|
3635
|
+
import { Program, Solver, Env, deref, variable, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
3636
|
+
const program = Program.parse(${JSON.stringify(':- use_module(library(lists)).\n')}, { sourceMetadata: false });
|
|
3637
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
3638
|
+
const goal = parseGoalText('catch(length(L,N),error(E,_),true),L=N', {
|
|
3639
|
+
operatorDefinitions: [...program.operators.values()],
|
|
3640
|
+
});
|
|
3641
|
+
let answer = null;
|
|
3642
|
+
for (const env of solver.solve([goal], new Env(), 0)) {
|
|
3643
|
+
answer = env;
|
|
3644
|
+
break;
|
|
3645
|
+
}
|
|
3646
|
+
if (answer == null) throw new Error('issue #49 query produced no caught answer');
|
|
3647
|
+
const formal = deref(variable('E'), answer);
|
|
3648
|
+
if (formal?.name !== 'resource_error' || deref(formal.args?.[0], answer)?.name !== 'memory') {
|
|
3649
|
+
throw new Error('unexpected caught error: ' + JSON.stringify(formal));
|
|
3650
|
+
}
|
|
3651
|
+
const left = deref(variable('L'), answer);
|
|
3652
|
+
const right = deref(variable('N'), answer);
|
|
3653
|
+
if (left.type !== 'var' || right.type !== 'var' || left.name !== right.name) {
|
|
3654
|
+
throw new Error('recovery did not leave L=N');
|
|
3655
|
+
}
|
|
3656
|
+
process.stdout.write('caught');
|
|
3657
|
+
`;
|
|
3658
|
+
const result = spawnSync(process.execPath, [
|
|
3659
|
+
'--max-old-space-size=64',
|
|
3660
|
+
'--input-type=module',
|
|
3661
|
+
'--eval',
|
|
3662
|
+
script,
|
|
3663
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 5000 });
|
|
3664
|
+
if (result.error) throw result.error;
|
|
3665
|
+
assertEqual(result.status, 0, `issue #49 bounded-heap child status; stderr=${result.stderr}`);
|
|
3666
|
+
assertEqual(result.stdout, 'caught', 'issue #49 resource error is caught by catch/3');
|
|
3667
|
+
},
|
|
3668
|
+
},
|
|
3630
3669
|
{
|
|
3631
3670
|
name: 'discarded fixed-length lists stay compact under a bounded heap',
|
|
3632
3671
|
run: () => {
|
|
@@ -4445,6 +4484,23 @@ function whiteBoxCases() {
|
|
|
4445
4484
|
assertEqual(ground.stats.datalog_evaluations, 0, 'ground query keeps the ordinary indexed chain path');
|
|
4446
4485
|
},
|
|
4447
4486
|
},
|
|
4487
|
+
{
|
|
4488
|
+
name: 'compact finite Datalog planning preserves lazy clause terms',
|
|
4489
|
+
run: () => {
|
|
4490
|
+
const facts = Array.from({ length: 130 }, (_, i) => `compact_edge(n${i}, n${i + 1}).`).join('\n');
|
|
4491
|
+
const source = `${facts}\ncompact_edge(X,Y) :- compact_edge(X,Y).\n`;
|
|
4492
|
+
const program = Program.parseSources([{ text: source, filename: 'compact-datalog.pl' }], {
|
|
4493
|
+
sourceMetadata: false,
|
|
4494
|
+
});
|
|
4495
|
+
const group = program.findGroup('compact_edge', 2);
|
|
4496
|
+
assertEqual(group.datalogLeastModel, true, 'compact recursive Datalog remains eligible');
|
|
4497
|
+
assertEqual(
|
|
4498
|
+
group.clauses.filter((clause) => clause._head != null || clause._body != null).length,
|
|
4499
|
+
0,
|
|
4500
|
+
'planning keeps compact clause terms lazy',
|
|
4501
|
+
);
|
|
4502
|
+
},
|
|
4503
|
+
},
|
|
4448
4504
|
{
|
|
4449
4505
|
name: 'findall length counting preserves multiplicity and observable bags',
|
|
4450
4506
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1900,9 +1900,15 @@ unification, another list predicate, or answer readback inspects it. This is a
|
|
|
1900
1900
|
storage optimization, not a distinct Prolog term or list semantics. Embedders
|
|
1901
1901
|
that inspect the JavaScript term model can recognize this representation with
|
|
1902
1902
|
`CompactListTerm`, `isCompactList`, and `compactListLength`, or construct one
|
|
1903
|
-
with `compactVariableList`.
|
|
1904
|
-
|
|
1905
|
-
|
|
1903
|
+
with `compactVariableList`. For open-ended `length(List, N)` generation, the
|
|
1904
|
+
bundled path binds each fresh generated spine directly instead of re-running an
|
|
1905
|
+
occurs-check over the whole growing list. It also reserves recovery headroom
|
|
1906
|
+
proportional to the retained spine, so a finite heap limit is raised inside the
|
|
1907
|
+
`length/2` search as a catchable `resource_error(memory)` rather than allowing
|
|
1908
|
+
an outer solver frame to encounter the limit first. The ordinary clauses remain
|
|
1909
|
+
the authoritative module definition and are used unchanged by the ISO-only
|
|
1910
|
+
registry and whenever delays or finite-domain constraints require their normal
|
|
1911
|
+
wake-up points.
|
|
1906
1912
|
|
|
1907
1913
|
### Implementation boundary
|
|
1908
1914
|
|