eyeprolog 1.3.4 → 1.3.7

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/src/program.js CHANGED
@@ -166,6 +166,9 @@ export class Program {
166
166
  tabled: false,
167
167
  recursive: false,
168
168
  tableInputPositions: [],
169
+ tableAllVariants: false,
170
+ datalogLeastModel: false,
171
+ wfsDatalog: false,
169
172
  scalarFactsOnly: true,
170
173
  dynamic: this.dynamicPredicates.has(modulePredicateKey(module, name, arity)),
171
174
  negationStratum: null,
@@ -308,6 +311,7 @@ export class Program {
308
311
  const deps = groups.map(() => new Set());
309
312
  const cutDeps = groups.map(() => new Set());
310
313
  const negativeEdges = [];
314
+ const wfsNegativeEdges = [];
311
315
  for (const group of groups) {
312
316
  const groupIndex = indexByGroup.get(group);
313
317
  for (const clause of group.clauses) {
@@ -337,12 +341,17 @@ export class Program {
337
341
  if (dep) {
338
342
  const dependencyIndex = indexByGroup.get(dep);
339
343
  deps[groupIndex].add(dependencyIndex);
340
- if (dependency.negative) negativeEdges.push([groupIndex, dependencyIndex]);
344
+ if (dependency.negative) {
345
+ negativeEdges.push([groupIndex, dependencyIndex]);
346
+ if (dependency.wfs === true) wfsNegativeEdges.push([groupIndex, dependencyIndex]);
347
+ }
341
348
  }
342
349
  }
343
350
  }
344
351
  }
345
352
  }
353
+ const finiteDatalogCache = new Map();
354
+ const rangeRestrictedDatalogCache = new Map();
346
355
  for (const group of groups) {
347
356
  const start = indexByGroup.get(group);
348
357
  const standardLibraryModule = group.module !== 'user' &&
@@ -386,6 +395,36 @@ export class Program {
386
395
  !componentHasNegativeEdge(start, deps, negativeEdges) &&
387
396
  !group.cutRecursive &&
388
397
  !linearNumeric;
398
+ // Function-free positive recursive rules have a finite Herbrand base.
399
+ // They can safely use a shared most-general table, which avoids computing
400
+ // a separate recursive closure for every bound input (TC/SG/Wine style).
401
+ group.tableAllVariants = group.tabled && isFiniteDatalogGroup(this, group, finiteDatalogCache) &&
402
+ datalogDependencyClauseCount(this, group) >= 128;
403
+ // Large finite, range-restricted positive Datalog can be evaluated as a
404
+ // single indexed least model. Keep tableAllVariants as the semantic
405
+ // fallback for finite programs whose answers may contain variables.
406
+ group.datalogLeastModel = group.tableAllVariants &&
407
+ isRangeRestrictedFiniteDatalogGroup(this, group, rangeRestrictedDatalogCache);
408
+ }
409
+
410
+ // Explicit tnot/1 opts finite, function-free Datalog into well-founded
411
+ // evaluation. Keep ISO \+/1 and not/1 on their existing negation-as-failure
412
+ // path: WFS is an extension with different three-valued semantics and must
413
+ // not silently change ordinary Prolog programs. A group is WFS-backed when
414
+ // its dependency cone reaches an SCC containing an edge introduced by
415
+ // tnot/1 and the whole cone is range-restricted finite Datalog.
416
+ const wfsCycleNodes = new Set();
417
+ for (const [from, to] of wfsNegativeEdges) {
418
+ if (!reachableIndexes(to, deps).has(from)) continue;
419
+ wfsCycleNodes.add(from);
420
+ wfsCycleNodes.add(to);
421
+ }
422
+ const finiteWfsCache = new Map();
423
+ for (const group of groups) {
424
+ const start = indexByGroup.get(group);
425
+ const reachable = reachableIndexes(start, deps);
426
+ const reachesWfsCycle = [...wfsCycleNodes].some((index) => reachable.has(index));
427
+ group.wfsDatalog = reachesWfsCycle && isFiniteWfsDatalogGroup(this, group, finiteWfsCache);
389
428
  }
390
429
  }
391
430
 
@@ -1253,6 +1292,185 @@ function reachableIndexes(start, deps) {
1253
1292
  return seen;
1254
1293
  }
1255
1294
 
1295
+
1296
+ function isFiniteDatalogArgument(term) {
1297
+ return term?.type === VAR || term?.type === ATOM || term?.type === 'string' || term?.type === 'number';
1298
+ }
1299
+
1300
+ function datalogDependencyClauseCount(program, group, seen = new Set()) {
1301
+ if (seen.has(group)) return 0;
1302
+ seen.add(group);
1303
+ let count = group.clauses.length;
1304
+ for (const clause of group.clauses) {
1305
+ 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
+ }
1310
+ continue;
1311
+ }
1312
+ for (const goal of clause.body) {
1313
+ if (goal.type !== COMPOUND && goal.type !== ATOM) continue;
1314
+ const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
1315
+ if (target) count += datalogDependencyClauseCount(program, target, seen);
1316
+ }
1317
+ }
1318
+ return count;
1319
+ }
1320
+
1321
+ function isFiniteDatalogGroup(program, group, cache = new Map(), visiting = new Set()) {
1322
+ const cached = cache.get(group);
1323
+ if (cached != null) return cached;
1324
+ if (visiting.has(group)) return true;
1325
+ visiting.add(group);
1326
+ let finite = true;
1327
+ for (const clause of group.clauses) {
1328
+ 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
+ }
1333
+ continue;
1334
+ }
1335
+ if (clause.head.type === COMPOUND && !clause.head.args.every(isFiniteDatalogArgument)) { finite = false; break; }
1336
+ if (clause.head.type !== COMPOUND && clause.head.type !== ATOM) { finite = false; break; }
1337
+ for (const goal of clause.body) {
1338
+ if (goal.type !== COMPOUND && goal.type !== ATOM) { finite = false; break; }
1339
+ if (goal.type === COMPOUND && !goal.args.every(isFiniteDatalogArgument)) { finite = false; break; }
1340
+ // Walk the whole positive dependency cone. Requiring every reachable
1341
+ // predicate to be source-defined and flat excludes builtins/generators
1342
+ // that could manufacture an unbounded stream of fresh terms.
1343
+ const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
1344
+ if (!target || !isFiniteDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
1345
+ }
1346
+ if (!finite) break;
1347
+ }
1348
+ visiting.delete(group);
1349
+ cache.set(group, finite);
1350
+ return finite;
1351
+ }
1352
+
1353
+
1354
+ function isRangeRestrictedFiniteDatalogGroup(program, group, cache = new Map(), visiting = new Set()) {
1355
+ const cached = cache.get(group);
1356
+ if (cached != null) return cached;
1357
+ if (visiting.has(group)) return true;
1358
+ visiting.add(group);
1359
+ let finite = true;
1360
+
1361
+ for (const clause of group.clauses) {
1362
+ const head = clause.head;
1363
+ if ((head.type !== COMPOUND && head.type !== ATOM) ||
1364
+ (head.type === COMPOUND && !head.args.every(isFiniteDatalogArgument))) {
1365
+ finite = false;
1366
+ break;
1367
+ }
1368
+
1369
+ const headVariables = new Set();
1370
+ const positiveVariables = new Set();
1371
+ collectVariables(head, headVariables);
1372
+ for (const goal of clause.body) {
1373
+ if ((goal.type !== COMPOUND && goal.type !== ATOM) ||
1374
+ (goal.type === COMPOUND && !goal.args.every(isFiniteDatalogArgument))) {
1375
+ finite = false;
1376
+ break;
1377
+ }
1378
+ collectVariables(goal, positiveVariables);
1379
+ const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
1380
+ if (!target || !isRangeRestrictedFiniteDatalogGroup(program, target, cache, visiting)) {
1381
+ finite = false;
1382
+ break;
1383
+ }
1384
+ }
1385
+ if (!finite) break;
1386
+ for (const name of headVariables) {
1387
+ if (!positiveVariables.has(name)) {
1388
+ // Ground facts have no head variables and pass naturally. A variable
1389
+ // fact or a rule with an unbound head variable needs ordinary Prolog
1390
+ // table semantics rather than finite relational materialization.
1391
+ finite = false;
1392
+ break;
1393
+ }
1394
+ }
1395
+ if (!finite) break;
1396
+ }
1397
+
1398
+ visiting.delete(group);
1399
+ cache.set(group, finite);
1400
+ return finite;
1401
+ }
1402
+
1403
+
1404
+ function isFiniteWfsDatalogGroup(program, group, cache = new Map(), visiting = new Set()) {
1405
+ const cached = cache.get(group);
1406
+ if (cached != null) return cached;
1407
+ if (visiting.has(group)) return true;
1408
+ visiting.add(group);
1409
+ let finite = true;
1410
+
1411
+ for (const clause of group.clauses) {
1412
+ if (isCompactBinaryClause(clause)) {
1413
+ if (clause.bodyName != null) {
1414
+ const target = program.findGroup(clause.bodyName, 2, group.module);
1415
+ if (!target || !isFiniteWfsDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
1416
+ }
1417
+ continue;
1418
+ }
1419
+ if ((clause.head.type !== COMPOUND && clause.head.type !== ATOM) ||
1420
+ (clause.head.type === COMPOUND && !clause.head.args.every(isFiniteDatalogArgument))) {
1421
+ finite = false;
1422
+ break;
1423
+ }
1424
+
1425
+ const positiveVariables = new Set();
1426
+ const requiredVariables = new Set();
1427
+ collectVariables(clause.head, requiredVariables);
1428
+ for (const goal of clause.body) {
1429
+ if (goal.type !== COMPOUND && goal.type !== ATOM) { finite = false; break; }
1430
+ if (goal.type === COMPOUND && goal.name === 'tnot' && goal.arity === 1) {
1431
+ const inner = goal.args[0];
1432
+ if ((inner.type !== COMPOUND && inner.type !== ATOM) ||
1433
+ (inner.type === COMPOUND && !inner.args.every(isFiniteDatalogArgument))) {
1434
+ finite = false;
1435
+ break;
1436
+ }
1437
+ collectVariables(inner, requiredVariables);
1438
+ const target = program.findGroup(inner.name, inner.arity, inner.module ?? group.module);
1439
+ if (!target || !isFiniteWfsDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
1440
+ continue;
1441
+ }
1442
+ // WFS is explicit. Ordinary \+/1 and not/1 retain NAF and therefore
1443
+ // disqualify this component from the WFS evaluator.
1444
+ if (goal.type === COMPOUND && (goal.name === '\\+' || goal.name === 'not')) {
1445
+ finite = false;
1446
+ break;
1447
+ }
1448
+ if (goal.type === COMPOUND && !goal.args.every(isFiniteDatalogArgument)) { finite = false; break; }
1449
+ collectVariables(goal, positiveVariables);
1450
+ const target = program.findGroup(goal.name, goal.arity, goal.module ?? group.module);
1451
+ if (!target || !isFiniteWfsDatalogGroup(program, target, cache, visiting)) { finite = false; break; }
1452
+ }
1453
+ if (!finite) break;
1454
+ for (const name of requiredVariables) {
1455
+ if (!positiveVariables.has(name)) { finite = false; break; }
1456
+ }
1457
+ if (!finite) break;
1458
+ }
1459
+
1460
+ visiting.delete(group);
1461
+ cache.set(group, finite);
1462
+ return finite;
1463
+ }
1464
+
1465
+ function collectVariables(term, output) {
1466
+ if (!term) return;
1467
+ if (term.type === VAR) {
1468
+ output.add(term.name);
1469
+ return;
1470
+ }
1471
+ for (const arg of term.args ?? []) collectVariables(arg, output);
1472
+ }
1473
+
1256
1474
  function inferStructuralInputPositions(group) {
1257
1475
  let firstPatternedPosition = -1;
1258
1476
  let firstLinkedInputPosition = -1;
@@ -1304,14 +1522,31 @@ function hasStrictListTailRecursion(group) {
1304
1522
  let foundRecursiveCall = false;
1305
1523
  for (const clause of group.clauses) {
1306
1524
  if (isCompactBinaryClause(clause)) return false;
1307
- for (const goal of clause.body) {
1525
+ for (let goalIndex = 0; goalIndex < clause.body.length; goalIndex++) {
1526
+ const goal = clause.body[goalIndex];
1308
1527
  if (goal.type !== COMPOUND || goal.name !== group.name || goal.arity !== group.arity) continue;
1309
1528
  foundRecursiveCall = true;
1310
1529
  const decreases = goal.args.some((argument, index) => {
1311
1530
  const head = clause.head.args[index];
1312
- return head?.type === COMPOUND && head.name === '.' && head.arity === 2 &&
1313
- head.args[1]?.type === VAR && argument?.type === VAR &&
1314
- head.args[1].name === argument.name;
1531
+ if (head?.type === COMPOUND && head.name === '.' && head.arity === 2 &&
1532
+ head.args[1]?.type === VAR && argument?.type === VAR &&
1533
+ head.args[1].name === argument.name) return true;
1534
+ // DCG expansion represents [_], nt as Input = [_|Tail], nt(Tail,...).
1535
+ // Treat that normalized prefix unification as the same strict list-tail
1536
+ // decrease so phrase/2-3 can choose a non-tabled fast path for new
1537
+ // inputs while retaining ordinary tabling for repeated invocations.
1538
+ if (head?.type !== VAR || argument?.type !== VAR) return false;
1539
+ for (let prefixIndex = 0; prefixIndex < goalIndex; prefixIndex++) {
1540
+ const prefix = clause.body[prefixIndex];
1541
+ if (prefix?.type !== COMPOUND || prefix.name !== '=' || prefix.arity !== 2) continue;
1542
+ for (const [left, right] of [[prefix.args[0], prefix.args[1]], [prefix.args[1], prefix.args[0]]]) {
1543
+ if (left?.type !== VAR || left.name !== head.name ||
1544
+ right?.type !== COMPOUND || right.name !== '.' || right.arity !== 2 ||
1545
+ right.args[1]?.type !== VAR) continue;
1546
+ if (right.args[1].name === argument.name) return true;
1547
+ }
1548
+ }
1549
+ return false;
1315
1550
  });
1316
1551
  if (!decreases) return false;
1317
1552
  }
@@ -1377,7 +1612,7 @@ function directGoalDependencyKey(goal) {
1377
1612
  if (goal.type === ATOM) return `${goal.name}/0`;
1378
1613
  if (goal.type !== COMPOUND) return null;
1379
1614
  if (goal.name === ',' && goal.arity === 2) return null;
1380
- if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) return null;
1615
+ if ((goal.name === '\\+' || goal.name === 'not' || goal.name === 'tnot') && goal.arity === 1) return null;
1381
1616
  if (goal.name === 'once' && goal.arity === 1) return null;
1382
1617
  if (goal.name === 'forall' && goal.arity === 2) return null;
1383
1618
  if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) return null;
@@ -1386,43 +1621,46 @@ function directGoalDependencyKey(goal) {
1386
1621
  return `${goal.name}/${goal.arity}`;
1387
1622
  }
1388
1623
 
1389
- function collectGoalDependencies(goal, negated, traverseConditionals = false) {
1390
- if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated }];
1624
+ function collectGoalDependencies(goal, negated, traverseConditionals = false, wfs = false) {
1625
+ if (goal.type === ATOM) return [{ key: `${goal.name}/0`, name: goal.name, arity: 0, module: goal.module, negative: negated, wfs }];
1391
1626
  if (goal.type !== COMPOUND) return [];
1392
1627
  if (goal.name === ',' && goal.arity === 2) {
1393
1628
  return [
1394
- ...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
1395
- ...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
1629
+ ...collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs),
1630
+ ...collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs),
1396
1631
  ];
1397
1632
  }
1398
1633
  if (traverseConditionals && (goal.name === ';' || goal.name === '->') && goal.arity === 2) {
1399
1634
  return [
1400
- ...collectGoalDependencies(goal.args[0], negated, true),
1401
- ...collectGoalDependencies(goal.args[1], negated, true),
1635
+ ...collectGoalDependencies(goal.args[0], negated, true, wfs),
1636
+ ...collectGoalDependencies(goal.args[1], negated, true, wfs),
1402
1637
  ];
1403
1638
  }
1404
1639
  if ((goal.name === '\\+' || goal.name === 'not') && goal.arity === 1) {
1405
- return collectGoalDependencies(goal.args[0], !negated, traverseConditionals);
1640
+ return collectGoalDependencies(goal.args[0], !negated, traverseConditionals, wfs);
1641
+ }
1642
+ if (goal.name === 'tnot' && goal.arity === 1) {
1643
+ return collectGoalDependencies(goal.args[0], !negated, traverseConditionals, true);
1406
1644
  }
1407
1645
  if (goal.name === 'once' && goal.arity === 1) {
1408
- return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
1646
+ return collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs);
1409
1647
  }
1410
1648
  if (goal.name === 'forall' && goal.arity === 2) {
1411
1649
  return [
1412
- ...collectGoalDependencies(goal.args[0], negated, traverseConditionals),
1413
- ...collectGoalDependencies(goal.args[1], negated, traverseConditionals),
1650
+ ...collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs),
1651
+ ...collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs),
1414
1652
  ];
1415
1653
  }
1416
1654
  if ((goal.name === 'findall' || goal.name === 'sumall') && goal.arity === 3) {
1417
- return collectGoalDependencies(goal.args[1], negated, traverseConditionals);
1655
+ return collectGoalDependencies(goal.args[1], negated, traverseConditionals, wfs);
1418
1656
  }
1419
1657
  if (goal.name === 'countall' && goal.arity === 2) {
1420
- return collectGoalDependencies(goal.args[0], negated, traverseConditionals);
1658
+ return collectGoalDependencies(goal.args[0], negated, traverseConditionals, wfs);
1421
1659
  }
1422
1660
  if ((goal.name === 'aggregate_min' || goal.name === 'aggregate_max') && goal.arity === 5) {
1423
- return collectGoalDependencies(goal.args[2], negated, traverseConditionals);
1661
+ return collectGoalDependencies(goal.args[2], negated, traverseConditionals, wfs);
1424
1662
  }
1425
- return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated }];
1663
+ return [{ key: `${goal.name}/${goal.arity}`, name: goal.name, arity: goal.arity, module: goal.module, negative: negated, wfs }];
1426
1664
  }
1427
1665
 
1428
1666
  function stronglyConnectedComponents(adjacency) {