eyeling 1.16.2 → 1.16.3
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/HANDBOOK.md +4 -0
- package/examples/ershov-mixed-computation.n3 +106 -0
- package/examples/output/ershov-mixed-computation.n3 +15 -0
- package/eyeling.js +510 -263
- package/lib/cli.js +22 -12
- package/lib/engine.js +488 -251
- package/package.json +1 -1
package/lib/engine.js
CHANGED
|
@@ -30,7 +30,7 @@ const {
|
|
|
30
30
|
// In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
|
|
31
31
|
// Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
|
|
32
32
|
const RDF_NIL_IRI = RDF_NS + 'nil';
|
|
33
|
-
const
|
|
33
|
+
const EMPTY_LIST_TERM = new ListTerm([]);
|
|
34
34
|
|
|
35
35
|
const { lex, N3SyntaxError } = require('./lexer');
|
|
36
36
|
const { Parser } = require('./parser');
|
|
@@ -72,13 +72,13 @@ let version = 'dev';
|
|
|
72
72
|
try {
|
|
73
73
|
// Node: keep package.json version if available
|
|
74
74
|
if (typeof require === 'function') version = require('./package.json').version || version;
|
|
75
|
-
} catch
|
|
75
|
+
} catch {}
|
|
76
76
|
|
|
77
77
|
let nodeCrypto = null;
|
|
78
78
|
try {
|
|
79
79
|
// Node: crypto available
|
|
80
80
|
if (typeof require === 'function') nodeCrypto = require('crypto');
|
|
81
|
-
} catch
|
|
81
|
+
} catch {}
|
|
82
82
|
// For a single reasoning run, this maps a canonical representation
|
|
83
83
|
// of the subject term in log:skolem to a Skolem IRI.
|
|
84
84
|
const skolemCache = new Map();
|
|
@@ -90,10 +90,10 @@ const skolemCache = new Map();
|
|
|
90
90
|
// - Across reasoning runs (default): same subject -> different Skolem IRI.
|
|
91
91
|
// - Optional legacy mode: stable across runs (CLI: --deterministic-skolem).
|
|
92
92
|
let deterministicSkolemAcrossRuns = false;
|
|
93
|
-
let
|
|
94
|
-
let
|
|
93
|
+
let skolemRunDepth = 0;
|
|
94
|
+
let skolemRunSalt = null;
|
|
95
95
|
|
|
96
|
-
function
|
|
96
|
+
function makeSkolemRunSalt() {
|
|
97
97
|
// Prefer WebCrypto if present (browser/worker)
|
|
98
98
|
try {
|
|
99
99
|
const g = typeof globalThis !== 'undefined' ? globalThis : null;
|
|
@@ -107,7 +107,7 @@ function __makeSkolemRunSalt() {
|
|
|
107
107
|
.join('');
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
} catch
|
|
110
|
+
} catch {}
|
|
111
111
|
|
|
112
112
|
// Node.js crypto
|
|
113
113
|
try {
|
|
@@ -115,7 +115,7 @@ function __makeSkolemRunSalt() {
|
|
|
115
115
|
if (typeof nodeCrypto.randomUUID === 'function') return nodeCrypto.randomUUID();
|
|
116
116
|
if (typeof nodeCrypto.randomBytes === 'function') return nodeCrypto.randomBytes(16).toString('hex');
|
|
117
117
|
}
|
|
118
|
-
} catch
|
|
118
|
+
} catch {}
|
|
119
119
|
|
|
120
120
|
// Last-resort fallback (not cryptographically strong)
|
|
121
121
|
return (
|
|
@@ -123,30 +123,30 @@ function __makeSkolemRunSalt() {
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
if (
|
|
126
|
+
function enterReasoningRun() {
|
|
127
|
+
skolemRunDepth += 1;
|
|
128
|
+
if (skolemRunDepth === 1) {
|
|
129
129
|
skolemCache.clear();
|
|
130
|
-
|
|
130
|
+
skolemRunSalt = deterministicSkolemAcrossRuns ? '' : makeSkolemRunSalt();
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
function
|
|
135
|
-
if (
|
|
136
|
-
if (
|
|
134
|
+
function exitReasoningRun() {
|
|
135
|
+
if (skolemRunDepth > 0) skolemRunDepth -= 1;
|
|
136
|
+
if (skolemRunDepth === 0) {
|
|
137
137
|
// Clear the salt so a future top-level run gets a fresh one (default mode).
|
|
138
|
-
|
|
138
|
+
skolemRunSalt = null;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
function
|
|
142
|
+
function skolemIdForKey(key) {
|
|
143
143
|
if (deterministicSkolemAcrossRuns) return deterministicSkolemIdFromKey(key);
|
|
144
144
|
// Ensure we have a run salt even if log:skolem is invoked outside forwardChain().
|
|
145
|
-
if (
|
|
145
|
+
if (skolemRunSalt === null) {
|
|
146
146
|
skolemCache.clear();
|
|
147
|
-
|
|
147
|
+
skolemRunSalt = makeSkolemRunSalt();
|
|
148
148
|
}
|
|
149
|
-
return deterministicSkolemIdFromKey(
|
|
149
|
+
return deterministicSkolemIdFromKey(skolemRunSalt + '|' + key);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function getDeterministicSkolemEnabled() {
|
|
@@ -156,8 +156,8 @@ function getDeterministicSkolemEnabled() {
|
|
|
156
156
|
function setDeterministicSkolemEnabled(v) {
|
|
157
157
|
deterministicSkolemAcrossRuns = !!v;
|
|
158
158
|
// Reset per-run state so the new mode takes effect immediately for the next run.
|
|
159
|
-
if (
|
|
160
|
-
|
|
159
|
+
if (skolemRunDepth === 0) {
|
|
160
|
+
skolemRunSalt = null;
|
|
161
161
|
skolemCache.clear();
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -243,6 +243,7 @@ function __computeHeadIsStrictGround(r) {
|
|
|
243
243
|
if (r.isFuse) return false;
|
|
244
244
|
// Dynamic heads depend on runtime bindings; treat as non-ground.
|
|
245
245
|
if (r.__dynamicConclusionTerm) return false;
|
|
246
|
+
if (r.__fromRulePromotion) return false;
|
|
246
247
|
if (r.headBlankLabels && r.headBlankLabels.size) return false;
|
|
247
248
|
for (const tr of r.conclusion) if (!__isStrictGroundTriple(tr)) return false;
|
|
248
249
|
return true;
|
|
@@ -1118,10 +1119,13 @@ function candidateFacts(facts, goal) {
|
|
|
1118
1119
|
else if (wildPO) wild = wildPO;
|
|
1119
1120
|
else wild = facts.__wildPred.length ? facts.__wildPred : null;
|
|
1120
1121
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1122
|
+
return {
|
|
1123
|
+
exact: exact || null,
|
|
1124
|
+
wild: wild || null,
|
|
1125
|
+
exactLen: exact ? exact.length : 0,
|
|
1126
|
+
wildLen: wild ? wild.length : 0,
|
|
1127
|
+
totalLen: (exact ? exact.length : 0) + (wild ? wild.length : 0),
|
|
1128
|
+
};
|
|
1125
1129
|
}
|
|
1126
1130
|
|
|
1127
1131
|
return null;
|
|
@@ -1164,6 +1168,11 @@ function pushFactIndexed(facts, tr) {
|
|
|
1164
1168
|
indexFact(facts, tr, idx);
|
|
1165
1169
|
}
|
|
1166
1170
|
|
|
1171
|
+
function makeDerivedRecord(fact, rule, premises, subst, captureExplanations) {
|
|
1172
|
+
if (captureExplanations === false) return { fact };
|
|
1173
|
+
return new DerivedFact(fact, rule, premises.slice(), { ...subst });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1167
1176
|
function ensureBackRuleIndexes(backRules) {
|
|
1168
1177
|
if (backRules.__byHeadPred && backRules.__wildHeadPred) return;
|
|
1169
1178
|
|
|
@@ -1197,6 +1206,164 @@ function indexBackRule(backRules, r) {
|
|
|
1197
1206
|
}
|
|
1198
1207
|
}
|
|
1199
1208
|
|
|
1209
|
+
function isSinglePremiseAgendaRuleSafe(r, backRules) {
|
|
1210
|
+
if (!r || r.isFuse || !Array.isArray(r.premise) || r.premise.length !== 1) return false;
|
|
1211
|
+
|
|
1212
|
+
// Keep agenda firing restricted to rules whose observable output order is
|
|
1213
|
+
// already stable in the legacy engine. Dynamic heads and head-blank
|
|
1214
|
+
// skolemization are deliberately left on the old path so example outputs keep
|
|
1215
|
+
// the same derived blank labels and rule-promotion behavior.
|
|
1216
|
+
if (r.__dynamicConclusionTerm) return false;
|
|
1217
|
+
if (r.__fromRulePromotion) return false;
|
|
1218
|
+
if (r.headBlankLabels && r.headBlankLabels.size) return false;
|
|
1219
|
+
|
|
1220
|
+
const goal = r.premise[0];
|
|
1221
|
+
|
|
1222
|
+
// Builtin-only bodies need the normal proveGoals path because they can
|
|
1223
|
+
// succeed without matching an extensional fact and may depend on scoped state.
|
|
1224
|
+
if (isBuiltinPred(goal.p)) return false;
|
|
1225
|
+
|
|
1226
|
+
// Safe only when the sole premise cannot be satisfied via backward rules.
|
|
1227
|
+
// Otherwise matching just against newly-seen facts would be incomplete.
|
|
1228
|
+
ensureBackRuleIndexes(backRules);
|
|
1229
|
+
if (goal.p instanceof Iri) {
|
|
1230
|
+
if ((backRules.__byHeadPred.get(goal.p.__tid) || []).length) return false;
|
|
1231
|
+
if (backRules.__wildHeadPred.length) return false;
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return backRules.__wildHeadPred.length === 0;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function mergeSinglePremiseAgendaBuckets() {
|
|
1239
|
+
let out = null;
|
|
1240
|
+
let seen = null;
|
|
1241
|
+
|
|
1242
|
+
for (let i = 0; i < arguments.length; i++) {
|
|
1243
|
+
const bucket = arguments[i];
|
|
1244
|
+
if (!bucket || bucket.length === 0) continue;
|
|
1245
|
+
|
|
1246
|
+
if (out === null) {
|
|
1247
|
+
out = bucket.length === 1 ? [bucket[0]] : bucket.slice();
|
|
1248
|
+
if (bucket.length > 1) seen = new Set(out);
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (!seen) seen = new Set(out);
|
|
1253
|
+
for (let j = 0; j < bucket.length; j++) {
|
|
1254
|
+
const entry = bucket[j];
|
|
1255
|
+
if (seen.has(entry)) continue;
|
|
1256
|
+
seen.add(entry);
|
|
1257
|
+
out.push(entry);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
return out;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
|
|
1265
|
+
const index = {
|
|
1266
|
+
byPred: new Map(),
|
|
1267
|
+
byPS: new Map(),
|
|
1268
|
+
byPO: new Map(),
|
|
1269
|
+
wildPred: [],
|
|
1270
|
+
wildPS: new Map(),
|
|
1271
|
+
wildPO: new Map(),
|
|
1272
|
+
indexed: new Set(),
|
|
1273
|
+
size: 0,
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
function addToMapArray(m, k, v) {
|
|
1277
|
+
let bucket = m.get(k);
|
|
1278
|
+
if (!bucket) {
|
|
1279
|
+
bucket = [];
|
|
1280
|
+
m.set(k, bucket);
|
|
1281
|
+
}
|
|
1282
|
+
bucket.push(v);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
for (let i = 0; i < forwardRules.length; i++) {
|
|
1286
|
+
const r = forwardRules[i];
|
|
1287
|
+
if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
|
|
1288
|
+
|
|
1289
|
+
const goal = r.premise[0];
|
|
1290
|
+
const entry = {
|
|
1291
|
+
rule: r,
|
|
1292
|
+
ruleIndex: i,
|
|
1293
|
+
goal,
|
|
1294
|
+
goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
|
|
1295
|
+
goalSKey: termFastKey(goal.s),
|
|
1296
|
+
goalOKey: termFastKey(goal.o),
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
index.indexed.add(r);
|
|
1300
|
+
index.size += 1;
|
|
1301
|
+
|
|
1302
|
+
if (entry.goalPredTid !== null) {
|
|
1303
|
+
if (entry.goalSKey === null && entry.goalOKey === null) addToMapArray(index.byPred, entry.goalPredTid, entry);
|
|
1304
|
+
if (entry.goalSKey !== null) {
|
|
1305
|
+
let ps = index.byPS.get(entry.goalPredTid);
|
|
1306
|
+
if (!ps) {
|
|
1307
|
+
ps = new Map();
|
|
1308
|
+
index.byPS.set(entry.goalPredTid, ps);
|
|
1309
|
+
}
|
|
1310
|
+
addToMapArray(ps, entry.goalSKey, entry);
|
|
1311
|
+
}
|
|
1312
|
+
if (entry.goalOKey !== null) {
|
|
1313
|
+
let po = index.byPO.get(entry.goalPredTid);
|
|
1314
|
+
if (!po) {
|
|
1315
|
+
po = new Map();
|
|
1316
|
+
index.byPO.set(entry.goalPredTid, po);
|
|
1317
|
+
}
|
|
1318
|
+
addToMapArray(po, entry.goalOKey, entry);
|
|
1319
|
+
}
|
|
1320
|
+
} else {
|
|
1321
|
+
if (entry.goalSKey === null && entry.goalOKey === null) index.wildPred.push(entry);
|
|
1322
|
+
if (entry.goalSKey !== null) addToMapArray(index.wildPS, entry.goalSKey, entry);
|
|
1323
|
+
if (entry.goalOKey !== null) addToMapArray(index.wildPO, entry.goalOKey, entry);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return index;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function getSinglePremiseAgendaCandidates(index, fact) {
|
|
1331
|
+
if (!index || index.size === 0) return null;
|
|
1332
|
+
|
|
1333
|
+
const sk = termFastKey(fact.s);
|
|
1334
|
+
const ok = termFastKey(fact.o);
|
|
1335
|
+
|
|
1336
|
+
let exact = null;
|
|
1337
|
+
if (fact.p instanceof Iri) {
|
|
1338
|
+
const pk = fact.p.__tid;
|
|
1339
|
+
const byPred = index.byPred.get(pk) || null;
|
|
1340
|
+
let byPS = null;
|
|
1341
|
+
if (sk !== null) {
|
|
1342
|
+
const ps = index.byPS.get(pk);
|
|
1343
|
+
if (ps) byPS = ps.get(sk) || null;
|
|
1344
|
+
}
|
|
1345
|
+
let byPO = null;
|
|
1346
|
+
if (ok !== null) {
|
|
1347
|
+
const po = index.byPO.get(pk);
|
|
1348
|
+
if (po) byPO = po.get(ok) || null;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const wildPred = index.wildPred.length ? index.wildPred : null;
|
|
1355
|
+
let wildPS = null;
|
|
1356
|
+
if (sk !== null) wildPS = index.wildPS.get(sk) || null;
|
|
1357
|
+
|
|
1358
|
+
let wildPO = null;
|
|
1359
|
+
if (ok !== null) wildPO = index.wildPO.get(ok) || null;
|
|
1360
|
+
|
|
1361
|
+
const wild = mergeSinglePremiseAgendaBuckets(wildPred, wildPS, wildPO);
|
|
1362
|
+
|
|
1363
|
+
if (!exact && !wild) return null;
|
|
1364
|
+
return { exact, wild, exactLen: exact ? exact.length : 0, wildLen: wild ? wild.length : 0 };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1200
1367
|
// ===========================================================================
|
|
1201
1368
|
// Special predicate helpers
|
|
1202
1369
|
// ===========================================================================
|
|
@@ -1222,7 +1389,7 @@ function isLogImpliedBy(p) {
|
|
|
1222
1389
|
// So this improves reuse across repeated backward proofs without changing the
|
|
1223
1390
|
// semantics of recursive goals.
|
|
1224
1391
|
|
|
1225
|
-
function
|
|
1392
|
+
function goalTableScopeVersion(facts, backRules) {
|
|
1226
1393
|
const factCount = Array.isArray(facts) ? facts.length : 0;
|
|
1227
1394
|
const backRuleCount = Array.isArray(backRules) ? backRules.length : 0;
|
|
1228
1395
|
const scopedLevel = facts && typeof facts.__scopedClosureLevel === 'number' ? facts.__scopedClosureLevel : 0;
|
|
@@ -1239,26 +1406,26 @@ function __makeGoalTable() {
|
|
|
1239
1406
|
|
|
1240
1407
|
function __attachGoalTable(scopeCarrier, goalTable) {
|
|
1241
1408
|
if (!scopeCarrier) return goalTable;
|
|
1242
|
-
if (!hasOwn.call(scopeCarrier, '
|
|
1243
|
-
Object.defineProperty(scopeCarrier, '
|
|
1409
|
+
if (!hasOwn.call(scopeCarrier, 'goalTable')) {
|
|
1410
|
+
Object.defineProperty(scopeCarrier, 'goalTable', {
|
|
1244
1411
|
value: goalTable,
|
|
1245
1412
|
enumerable: false,
|
|
1246
1413
|
writable: true,
|
|
1247
1414
|
configurable: true,
|
|
1248
1415
|
});
|
|
1249
1416
|
} else {
|
|
1250
|
-
scopeCarrier.
|
|
1417
|
+
scopeCarrier.goalTable = goalTable;
|
|
1251
1418
|
}
|
|
1252
1419
|
return goalTable;
|
|
1253
1420
|
}
|
|
1254
1421
|
|
|
1255
1422
|
function __ensureGoalTable(facts, backRules) {
|
|
1256
|
-
let table = (facts && facts.
|
|
1423
|
+
let table = (facts && facts.goalTable) || (backRules && backRules.goalTable) || null;
|
|
1257
1424
|
if (!table) table = __makeGoalTable();
|
|
1258
1425
|
__attachGoalTable(facts, table);
|
|
1259
1426
|
__attachGoalTable(backRules, table);
|
|
1260
1427
|
|
|
1261
|
-
const version =
|
|
1428
|
+
const version = goalTableScopeVersion(facts, backRules);
|
|
1262
1429
|
if (table.scopeVersion !== version) {
|
|
1263
1430
|
table.scopeVersion = version;
|
|
1264
1431
|
table.entries.clear();
|
|
@@ -1304,6 +1471,7 @@ function __canStoreGoalMemo(visited, maxResults) {
|
|
|
1304
1471
|
// ===========================================================================
|
|
1305
1472
|
|
|
1306
1473
|
function containsVarTerm(t, v) {
|
|
1474
|
+
if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return false;
|
|
1307
1475
|
if (t instanceof Var) return t.name === v;
|
|
1308
1476
|
if (t instanceof ListTerm) return t.elems.some((e) => containsVarTerm(e, v));
|
|
1309
1477
|
if (t instanceof OpenListTerm) return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
|
|
@@ -1327,6 +1495,7 @@ function isGroundTripleInGraph(tr) {
|
|
|
1327
1495
|
}
|
|
1328
1496
|
|
|
1329
1497
|
function isGroundTerm(t) {
|
|
1498
|
+
if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return true;
|
|
1330
1499
|
if (t instanceof Var) return false;
|
|
1331
1500
|
if (t instanceof ListTerm) return t.elems.every((e) => isGroundTerm(e));
|
|
1332
1501
|
if (t instanceof OpenListTerm) return false;
|
|
@@ -1360,7 +1529,7 @@ function skolemIriFromGroundTerm(t) {
|
|
|
1360
1529
|
const key = skolemKeyFromTerm(t);
|
|
1361
1530
|
let iri = skolemCache.get(key);
|
|
1362
1531
|
if (!iri) {
|
|
1363
|
-
const id =
|
|
1532
|
+
const id = skolemIdForKey(key);
|
|
1364
1533
|
iri = internIri(SKOLEM_NS + id);
|
|
1365
1534
|
skolemCache.set(key, iri);
|
|
1366
1535
|
}
|
|
@@ -1368,6 +1537,9 @@ function skolemIriFromGroundTerm(t) {
|
|
|
1368
1537
|
}
|
|
1369
1538
|
|
|
1370
1539
|
function applySubstTerm(t, s) {
|
|
1540
|
+
// Hot fast path: most terms are already-ground atomic terms.
|
|
1541
|
+
if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return t;
|
|
1542
|
+
|
|
1371
1543
|
// Common case: variable
|
|
1372
1544
|
if (t instanceof Var) {
|
|
1373
1545
|
const first = s[t.name];
|
|
@@ -1562,8 +1734,8 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1562
1734
|
|
|
1563
1735
|
// Normalize rdf:nil IRI to the empty list term, so it unifies with () and
|
|
1564
1736
|
// list builtins treat it consistently.
|
|
1565
|
-
if (a instanceof Iri && a.value === RDF_NIL_IRI) a =
|
|
1566
|
-
if (b instanceof Iri && b.value === RDF_NIL_IRI) b =
|
|
1737
|
+
if (a instanceof Iri && a.value === RDF_NIL_IRI) a = EMPTY_LIST_TERM;
|
|
1738
|
+
if (b instanceof Iri && b.value === RDF_NIL_IRI) b = EMPTY_LIST_TERM;
|
|
1567
1739
|
|
|
1568
1740
|
// Variable binding
|
|
1569
1741
|
if (a instanceof Var) {
|
|
@@ -1793,10 +1965,10 @@ function __builtinIsSatisfiableWhenFullyUnbound(pIriVal) {
|
|
|
1793
1965
|
}
|
|
1794
1966
|
|
|
1795
1967
|
function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxResults, opts) {
|
|
1796
|
-
const
|
|
1797
|
-
const
|
|
1798
|
-
if (
|
|
1799
|
-
const cached =
|
|
1968
|
+
const goalTable = __canLookupGoalMemo(visited) ? __ensureGoalTable(facts, backRules) : null;
|
|
1969
|
+
const goalMemoKeyNow = goalTable ? __goalMemoKey(goals, subst, facts, opts) : null;
|
|
1970
|
+
if (goalTable && goalTable.entries.has(goalMemoKeyNow)) {
|
|
1971
|
+
const cached = goalTable.entries.get(goalMemoKeyNow) || [];
|
|
1800
1972
|
const cloned = __cloneGoalSolutions(cached);
|
|
1801
1973
|
if (typeof maxResults === 'number' && maxResults > 0 && cloned.length > maxResults)
|
|
1802
1974
|
return cloned.slice(0, maxResults);
|
|
@@ -1815,7 +1987,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1815
1987
|
|
|
1816
1988
|
// IMPORTANT: Goal reordering / deferral is only enabled when explicitly
|
|
1817
1989
|
// requested by the caller (used for forward rules).
|
|
1818
|
-
const
|
|
1990
|
+
const allowDeferredBuiltins = !!(opts && opts.deferBuiltins);
|
|
1819
1991
|
|
|
1820
1992
|
const initialGoals = Array.isArray(goals) ? goals.slice() : [];
|
|
1821
1993
|
const substMut = subst ? { ...subst } : {};
|
|
@@ -1830,8 +2002,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1830
2002
|
|
|
1831
2003
|
if (!initialGoals.length) {
|
|
1832
2004
|
results.push(gcCompactForGoals(substMut, [], answerVars));
|
|
1833
|
-
if (
|
|
1834
|
-
|
|
2005
|
+
if (goalTable && __canStoreGoalMemo(visited, maxResults)) {
|
|
2006
|
+
goalTable.entries.set(goalMemoKeyNow, __cloneGoalSolutions(results));
|
|
1835
2007
|
}
|
|
1836
2008
|
return results;
|
|
1837
2009
|
}
|
|
@@ -1871,14 +2043,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1871
2043
|
const visitedCounts = new Map(); // key -> count
|
|
1872
2044
|
const visitedTrail = []; // stack of keys in insertion order
|
|
1873
2045
|
|
|
1874
|
-
const
|
|
2046
|
+
const termKeyCache = typeof WeakMap === 'function' ? new WeakMap() : null;
|
|
1875
2047
|
|
|
1876
|
-
function
|
|
2048
|
+
function termKeyForVisited(t) {
|
|
1877
2049
|
if (t instanceof Iri && t.value === RDF_NIL_IRI) return '()';
|
|
1878
2050
|
if (t instanceof ListTerm && t.elems.length === 0) return '()';
|
|
1879
2051
|
|
|
1880
|
-
if (
|
|
1881
|
-
const cached =
|
|
2052
|
+
if (termKeyCache && t && typeof t === 'object') {
|
|
2053
|
+
const cached = termKeyCache.get(t);
|
|
1882
2054
|
if (cached) return cached;
|
|
1883
2055
|
}
|
|
1884
2056
|
|
|
@@ -1909,14 +2081,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1909
2081
|
// Iri / Blank and other atomic interned terms
|
|
1910
2082
|
out = 'T' + t.__tid;
|
|
1911
2083
|
} else if (t instanceof ListTerm) {
|
|
1912
|
-
out = '[' + t.elems.map(
|
|
2084
|
+
out = '[' + t.elems.map(termKeyForVisited).join(',') + ']';
|
|
1913
2085
|
} else if (t instanceof OpenListTerm) {
|
|
1914
|
-
out = '[open:' + t.prefix.map(
|
|
2086
|
+
out = '[open:' + t.prefix.map(termKeyForVisited).join(',') + '|tail:' + t.tailVar + ']';
|
|
1915
2087
|
} else if (t instanceof GraphTerm) {
|
|
1916
2088
|
out =
|
|
1917
2089
|
'{' +
|
|
1918
2090
|
t.triples
|
|
1919
|
-
.map((tr) =>
|
|
2091
|
+
.map((tr) => termKeyForVisited(tr.s) + ' ' + termKeyForVisited(tr.p) + ' ' + termKeyForVisited(tr.o))
|
|
1920
2092
|
.join(';') +
|
|
1921
2093
|
'}';
|
|
1922
2094
|
} else {
|
|
@@ -1924,20 +2096,20 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1924
2096
|
out = skolemKeyFromTerm(t);
|
|
1925
2097
|
}
|
|
1926
2098
|
|
|
1927
|
-
if (
|
|
2099
|
+
if (termKeyCache && t && typeof t === 'object') termKeyCache.set(t, out);
|
|
1928
2100
|
return out;
|
|
1929
2101
|
}
|
|
1930
2102
|
|
|
1931
|
-
function
|
|
1932
|
-
return
|
|
2103
|
+
function tripleKeyForVisited(tr) {
|
|
2104
|
+
return termKeyForVisited(tr.s) + '\t' + termKeyForVisited(tr.p) + '\t' + termKeyForVisited(tr.o);
|
|
1933
2105
|
}
|
|
1934
2106
|
|
|
1935
|
-
function
|
|
2107
|
+
function pushVisitedKey(key) {
|
|
1936
2108
|
visitedTrail.push(key);
|
|
1937
2109
|
visitedCounts.set(key, (visitedCounts.get(key) || 0) + 1);
|
|
1938
2110
|
}
|
|
1939
2111
|
|
|
1940
|
-
function
|
|
2112
|
+
function undoVisitedKeysTo(mark) {
|
|
1941
2113
|
for (let i = visitedTrail.length - 1; i >= mark; i--) {
|
|
1942
2114
|
const k = visitedTrail[i];
|
|
1943
2115
|
const c = visitedCounts.get(k);
|
|
@@ -1947,7 +2119,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1947
2119
|
visitedTrail.length = mark;
|
|
1948
2120
|
}
|
|
1949
2121
|
|
|
1950
|
-
for (const tr of initialVisited)
|
|
2122
|
+
for (const tr of initialVisited) pushVisitedKey(tripleKeyForVisited(tr));
|
|
1951
2123
|
|
|
1952
2124
|
// ---------------------------------------------------------------------------
|
|
1953
2125
|
// In-place unification into the mutable substitution + trail.
|
|
@@ -1979,8 +2151,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1979
2151
|
|
|
1980
2152
|
// Normalize rdf:nil IRI to the empty list term, so it unifies with () and
|
|
1981
2153
|
// list builtins treat it consistently.
|
|
1982
|
-
if (a instanceof Iri && a.value === RDF_NIL_IRI) a =
|
|
1983
|
-
if (b instanceof Iri && b.value === RDF_NIL_IRI) b =
|
|
2154
|
+
if (a instanceof Iri && a.value === RDF_NIL_IRI) a = EMPTY_LIST_TERM;
|
|
2155
|
+
if (b instanceof Iri && b.value === RDF_NIL_IRI) b = EMPTY_LIST_TERM;
|
|
2156
|
+
|
|
2157
|
+
if (a === b) return true;
|
|
1984
2158
|
|
|
1985
2159
|
// Variable binding
|
|
1986
2160
|
if (a instanceof Var) return bindVarTrail(a.name, b);
|
|
@@ -2099,7 +2273,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2099
2273
|
kind: 'node',
|
|
2100
2274
|
goalsNow: initialGoals,
|
|
2101
2275
|
curDepth: depth || 0,
|
|
2102
|
-
canDeferBuiltins:
|
|
2276
|
+
canDeferBuiltins: allowDeferredBuiltins,
|
|
2103
2277
|
deferCount: 0,
|
|
2104
2278
|
});
|
|
2105
2279
|
|
|
@@ -2108,7 +2282,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2108
2282
|
|
|
2109
2283
|
if (frame.kind === 'undo') {
|
|
2110
2284
|
undoTo(frame.substMark);
|
|
2111
|
-
|
|
2285
|
+
undoVisitedKeysTo(frame.visitedMark);
|
|
2112
2286
|
continue;
|
|
2113
2287
|
}
|
|
2114
2288
|
|
|
@@ -2167,15 +2341,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2167
2341
|
// still preventing trivial non-termination in mutually recursive rule
|
|
2168
2342
|
// cycles.
|
|
2169
2343
|
if (frame.goalWasVisited && rStd.premise && rStd.premise.length) {
|
|
2170
|
-
let
|
|
2344
|
+
let hasCycle = false;
|
|
2171
2345
|
for (let i = 0; i < rStd.premise.length; i++) {
|
|
2172
|
-
const premKey =
|
|
2346
|
+
const premKey = tripleKeyForVisited(applySubstTriple(rStd.premise[i], substMut));
|
|
2173
2347
|
if (visitedCounts.has(premKey)) {
|
|
2174
|
-
|
|
2348
|
+
hasCycle = true;
|
|
2175
2349
|
break;
|
|
2176
2350
|
}
|
|
2177
2351
|
}
|
|
2178
|
-
if (
|
|
2352
|
+
if (hasCycle) {
|
|
2179
2353
|
undoTo(mark);
|
|
2180
2354
|
continue;
|
|
2181
2355
|
}
|
|
@@ -2184,7 +2358,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2184
2358
|
const newGoals = rStd.premise.concat(frame.restGoals);
|
|
2185
2359
|
|
|
2186
2360
|
const vMark = visitedTrail.length;
|
|
2187
|
-
|
|
2361
|
+
pushVisitedKey(frame.goalKey);
|
|
2188
2362
|
|
|
2189
2363
|
// Explore the rule body; then undo; then resume trying further rules.
|
|
2190
2364
|
stack.push(frame);
|
|
@@ -2206,8 +2380,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2206
2380
|
const candidates = frame.candidates;
|
|
2207
2381
|
const isIndexed = !!candidates;
|
|
2208
2382
|
|
|
2209
|
-
while (frame.idx < (isIndexed ? candidates.
|
|
2210
|
-
|
|
2383
|
+
while (frame.idx < (isIndexed ? candidates.totalLen : factsList.length) && results.length < max) {
|
|
2384
|
+
let f;
|
|
2385
|
+
if (isIndexed) {
|
|
2386
|
+
const idxNow = frame.idx++;
|
|
2387
|
+
if (idxNow < candidates.exactLen) f = factsList[candidates.exact[idxNow]];
|
|
2388
|
+
else f = factsList[candidates.wild[idxNow - candidates.exactLen]];
|
|
2389
|
+
} else {
|
|
2390
|
+
f = factsList[frame.idx++];
|
|
2391
|
+
}
|
|
2211
2392
|
|
|
2212
2393
|
const mark = trail.length;
|
|
2213
2394
|
if (!unifyTripleTrail(frame.goal0, f)) {
|
|
@@ -2250,13 +2431,13 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2250
2431
|
const goal0 = applySubstTriple(rawGoal, substMut);
|
|
2251
2432
|
|
|
2252
2433
|
// 1) Builtins
|
|
2253
|
-
const
|
|
2254
|
-
const
|
|
2255
|
-
const
|
|
2434
|
+
const goalPredicateIri = goal0.p instanceof Iri ? goal0.p.value : null;
|
|
2435
|
+
const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
|
|
2436
|
+
const shouldTreatAsBuiltin =
|
|
2256
2437
|
isBuiltinPred(goal0.p) &&
|
|
2257
|
-
!(
|
|
2438
|
+
!(isRdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
|
|
2258
2439
|
|
|
2259
|
-
if (
|
|
2440
|
+
if (shouldTreatAsBuiltin) {
|
|
2260
2441
|
const remaining = max - results.length;
|
|
2261
2442
|
if (remaining <= 0) continue;
|
|
2262
2443
|
const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
|
|
@@ -2264,11 +2445,11 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2264
2445
|
let deltas = evalBuiltin(goal0, {}, facts, backRules, frame.curDepth, varGen, builtinMax);
|
|
2265
2446
|
|
|
2266
2447
|
const dc = typeof frame.deferCount === 'number' ? frame.deferCount : 0;
|
|
2267
|
-
const
|
|
2448
|
+
const builtinDeltasAreVacuous = deltas.length > 0 && deltas.every((d) => Object.keys(d).length === 0);
|
|
2268
2449
|
|
|
2269
2450
|
if (
|
|
2270
2451
|
frame.canDeferBuiltins &&
|
|
2271
|
-
(!deltas.length ||
|
|
2452
|
+
(!deltas.length || builtinDeltasAreVacuous) &&
|
|
2272
2453
|
restGoals.length &&
|
|
2273
2454
|
__tripleHasVarOrBlank(goal0) &&
|
|
2274
2455
|
dc < goalsNow.length
|
|
@@ -2284,14 +2465,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2284
2465
|
continue;
|
|
2285
2466
|
}
|
|
2286
2467
|
|
|
2287
|
-
const
|
|
2468
|
+
const subjectAndObjectAreFullyUnbound =
|
|
2288
2469
|
(goal0.s instanceof Var || goal0.s instanceof Blank) && (goal0.o instanceof Var || goal0.o instanceof Blank);
|
|
2289
2470
|
|
|
2290
2471
|
if (
|
|
2291
2472
|
frame.canDeferBuiltins &&
|
|
2292
2473
|
!deltas.length &&
|
|
2293
|
-
__builtinIsSatisfiableWhenFullyUnbound(
|
|
2294
|
-
|
|
2474
|
+
__builtinIsSatisfiableWhenFullyUnbound(goalPredicateIri) &&
|
|
2475
|
+
subjectAndObjectAreFullyUnbound &&
|
|
2295
2476
|
(!restGoals.length || dc >= goalsNow.length)
|
|
2296
2477
|
) {
|
|
2297
2478
|
deltas = [{}];
|
|
@@ -2326,7 +2507,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2326
2507
|
// We therefore *allow* re-entering a visited goal, but when a goal is
|
|
2327
2508
|
// already visited we avoid applying backward rules whose premises would
|
|
2328
2509
|
// immediately re-enter any visited goal again (a cheap cycle guard).
|
|
2329
|
-
const goalKey =
|
|
2510
|
+
const goalKey = tripleKeyForVisited(goal0);
|
|
2330
2511
|
const goalWasVisited = visitedCounts.has(goalKey);
|
|
2331
2512
|
|
|
2332
2513
|
// 3) Backward rules (indexed by head predicate) — explored first
|
|
@@ -2376,8 +2557,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2376
2557
|
}
|
|
2377
2558
|
}
|
|
2378
2559
|
|
|
2379
|
-
if (
|
|
2380
|
-
|
|
2560
|
+
if (goalTable && __canStoreGoalMemo(visited, maxResults)) {
|
|
2561
|
+
goalTable.entries.set(goalMemoKeyNow, __cloneGoalSolutions(results));
|
|
2381
2562
|
}
|
|
2382
2563
|
|
|
2383
2564
|
return results;
|
|
@@ -2387,8 +2568,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2387
2568
|
// Forward chaining to fixpoint
|
|
2388
2569
|
// ===========================================================================
|
|
2389
2570
|
|
|
2390
|
-
function forwardChain(facts, forwardRules, backRules, onDerived /* optional
|
|
2391
|
-
|
|
2571
|
+
function forwardChain(facts, forwardRules, backRules, onDerived /* optional */, opts = {}) {
|
|
2572
|
+
enterReasoningRun();
|
|
2392
2573
|
try {
|
|
2393
2574
|
ensureFactIndexes(facts);
|
|
2394
2575
|
ensureBackRuleIndexes(backRules);
|
|
@@ -2397,7 +2578,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
2397
2578
|
__attachGoalTable(facts, goalTable);
|
|
2398
2579
|
__attachGoalTable(backRules, goalTable);
|
|
2399
2580
|
|
|
2400
|
-
const
|
|
2581
|
+
const captureExplanations = !(opts && opts.captureExplanations === false);
|
|
2401
2582
|
const derivedForward = [];
|
|
2402
2583
|
const varGen = [0];
|
|
2403
2584
|
const skCounter = [0];
|
|
@@ -2473,46 +2654,216 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
2473
2654
|
return snap;
|
|
2474
2655
|
}
|
|
2475
2656
|
|
|
2657
|
+
function __skipForwardRuleNow(r) {
|
|
2658
|
+
// Skip forward rules that are guaranteed to "delay" due to scoped
|
|
2659
|
+
// builtins (log:collectAllIn / log:forAllIn / log:includes / log:notIncludes)
|
|
2660
|
+
// until a snapshot exists (and a certain closure level is reached).
|
|
2661
|
+
// This prevents expensive proofs that will definitely fail in Phase A
|
|
2662
|
+
// and in early closure levels.
|
|
2663
|
+
const info = r.__scopedSkipInfo;
|
|
2664
|
+
if (info && info.needsSnap) {
|
|
2665
|
+
const snapHere = facts.__scopedSnapshot || null;
|
|
2666
|
+
const lvlHere = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
|
|
2667
|
+
if (!snapHere) return true;
|
|
2668
|
+
if (lvlHere < info.requiredLevel) return true;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// Optimization: if the rule head is **structurally ground** (no vars anywhere, even inside
|
|
2672
|
+
// quoted formulas) and has no head blanks, then the head does not depend on which body
|
|
2673
|
+
// solution we pick. In that case, we only need *one* proof of the body, and once all head
|
|
2674
|
+
// triples are already known we can skip proving the body entirely.
|
|
2675
|
+
const headIsStrictGround = r.__headIsStrictGround;
|
|
2676
|
+
if (headIsStrictGround) {
|
|
2677
|
+
let allKnown = true;
|
|
2678
|
+
for (const tr of r.conclusion) {
|
|
2679
|
+
if (!hasFactIndexed(facts, tr)) {
|
|
2680
|
+
allKnown = false;
|
|
2681
|
+
break;
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (allKnown) return true;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
return false;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
function __emitForwardRuleSolution(r, ruleIndex, s) {
|
|
2691
|
+
let changedHere = false;
|
|
2692
|
+
let rulesChanged = false;
|
|
2693
|
+
|
|
2694
|
+
// IMPORTANT: one skolem map per *rule firing*
|
|
2695
|
+
const skMap = {};
|
|
2696
|
+
const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
|
|
2697
|
+
const fireKey = __firingKey(ruleIndex, instantiatedPremises);
|
|
2698
|
+
|
|
2699
|
+
// Support "dynamic" rule heads where the consequent is a term that
|
|
2700
|
+
// (after substitution) evaluates to a quoted formula.
|
|
2701
|
+
// Example: { :a :b ?C } => ?C.
|
|
2702
|
+
let dynamicHeadTriples = null;
|
|
2703
|
+
let headBlankLabelsHere = r.headBlankLabels;
|
|
2704
|
+
if (r.__dynamicConclusionTerm) {
|
|
2705
|
+
const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
|
|
2706
|
+
|
|
2707
|
+
// Allow dynamic fuses: ... => ?X. where ?X becomes false
|
|
2708
|
+
if (dynTerm instanceof Literal && dynTerm.value === 'false') {
|
|
2709
|
+
console.log('# Inference fuse triggered: dynamic head resolved to false.');
|
|
2710
|
+
process.exit(2);
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
const dynTriples = __graphTriplesOrTrue(dynTerm);
|
|
2714
|
+
dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
|
|
2715
|
+
|
|
2716
|
+
// If the dynamic head contains explicit blank nodes, treat them as
|
|
2717
|
+
// head blanks for skolemization.
|
|
2718
|
+
const dynHeadBlankLabels =
|
|
2719
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? collectBlankLabelsInTriples(dynamicHeadTriples) : null;
|
|
2720
|
+
if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
|
|
2721
|
+
headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
const headPatterns =
|
|
2726
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
|
|
2727
|
+
|
|
2728
|
+
for (const cpat of headPatterns) {
|
|
2729
|
+
const instantiated = applySubstTriple(cpat, s);
|
|
2730
|
+
|
|
2731
|
+
const subj = instantiated.s;
|
|
2732
|
+
const obj = instantiated.o;
|
|
2733
|
+
|
|
2734
|
+
const subjIsGraph = subj instanceof GraphTerm;
|
|
2735
|
+
const objIsGraph = obj instanceof GraphTerm;
|
|
2736
|
+
const subjIsTrue = subj instanceof Literal && subj.value === 'true';
|
|
2737
|
+
const objIsTrue = obj instanceof Literal && obj.value === 'true';
|
|
2738
|
+
|
|
2739
|
+
const isFwRuleTriple =
|
|
2740
|
+
isLogImplies(instantiated.p) &&
|
|
2741
|
+
((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
|
|
2742
|
+
|
|
2743
|
+
const isBwRuleTriple =
|
|
2744
|
+
isLogImpliedBy(instantiated.p) &&
|
|
2745
|
+
((subjIsGraph && objIsGraph) || (subjIsGraph && objIsTrue) || (subjIsTrue && objIsGraph));
|
|
2746
|
+
|
|
2747
|
+
if (isFwRuleTriple || isBwRuleTriple) {
|
|
2748
|
+
if (!hasFactIndexed(facts, instantiated)) {
|
|
2749
|
+
pushFactIndexed(facts, instantiated);
|
|
2750
|
+
const df = makeDerivedRecord(instantiated, r, instantiatedPremises, s, captureExplanations);
|
|
2751
|
+
derivedForward.push(df);
|
|
2752
|
+
if (typeof onDerived === 'function') onDerived(df);
|
|
2753
|
+
changedHere = true;
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// Promote rule-producing triples to live rules, treating literal true as {}.
|
|
2757
|
+
const left = __graphTriplesOrTrue(subj);
|
|
2758
|
+
const right = __graphTriplesOrTrue(obj);
|
|
2759
|
+
|
|
2760
|
+
if (left !== null && right !== null) {
|
|
2761
|
+
if (isFwRuleTriple) {
|
|
2762
|
+
const [premise, conclusion] = liftBlankRuleVars(left, right);
|
|
2763
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
2764
|
+
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
2765
|
+
__prepareForwardRule(newRule);
|
|
2766
|
+
|
|
2767
|
+
const key = __ruleKey(
|
|
2768
|
+
newRule.isForward,
|
|
2769
|
+
newRule.isFuse,
|
|
2770
|
+
newRule.premise,
|
|
2771
|
+
newRule.conclusion,
|
|
2772
|
+
newRule.__dynamicConclusionTerm || null,
|
|
2773
|
+
);
|
|
2774
|
+
if (!forwardRules.__ruleKeySet.has(key)) {
|
|
2775
|
+
forwardRules.__ruleKeySet.add(key);
|
|
2776
|
+
forwardRules.push(newRule);
|
|
2777
|
+
rulesChanged = true;
|
|
2778
|
+
}
|
|
2779
|
+
} else if (isBwRuleTriple) {
|
|
2780
|
+
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
2781
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
2782
|
+
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
2783
|
+
|
|
2784
|
+
const key = __ruleKey(
|
|
2785
|
+
newRule.isForward,
|
|
2786
|
+
newRule.isFuse,
|
|
2787
|
+
newRule.premise,
|
|
2788
|
+
newRule.conclusion,
|
|
2789
|
+
newRule.__dynamicConclusionTerm || null,
|
|
2790
|
+
);
|
|
2791
|
+
if (!backRules.__ruleKeySet.has(key)) {
|
|
2792
|
+
backRules.__ruleKeySet.add(key);
|
|
2793
|
+
backRules.push(newRule);
|
|
2794
|
+
indexBackRule(backRules, newRule);
|
|
2795
|
+
rulesChanged = true;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
continue; // skip normal fact handling
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// Only skolemize blank nodes that occur explicitly in the rule head
|
|
2804
|
+
const inst = skolemizeTripleForHeadBlanks(
|
|
2805
|
+
instantiated,
|
|
2806
|
+
headBlankLabelsHere,
|
|
2807
|
+
skMap,
|
|
2808
|
+
skCounter,
|
|
2809
|
+
fireKey,
|
|
2810
|
+
headSkolemCache,
|
|
2811
|
+
);
|
|
2812
|
+
|
|
2813
|
+
if (!isGroundTriple(inst)) continue;
|
|
2814
|
+
if (hasFactIndexed(facts, inst)) continue;
|
|
2815
|
+
|
|
2816
|
+
pushFactIndexed(facts, inst);
|
|
2817
|
+
const df = makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations);
|
|
2818
|
+
derivedForward.push(df);
|
|
2819
|
+
if (typeof onDerived === 'function') onDerived(df);
|
|
2820
|
+
|
|
2821
|
+
changedHere = true;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
return { changedHere, rulesChanged };
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2476
2827
|
function runFixpoint() {
|
|
2477
2828
|
let anyChange = false;
|
|
2829
|
+
let agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
|
|
2830
|
+
let agendaCursor = 0;
|
|
2478
2831
|
|
|
2479
2832
|
while (true) {
|
|
2480
2833
|
let changed = false;
|
|
2481
2834
|
|
|
2482
|
-
|
|
2483
|
-
const
|
|
2835
|
+
while (agendaCursor < facts.length && agendaIndex.size) {
|
|
2836
|
+
const fact = facts[agendaCursor++];
|
|
2837
|
+
const candidates = getSinglePremiseAgendaCandidates(agendaIndex, fact);
|
|
2838
|
+
if (!candidates) continue;
|
|
2484
2839
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
const info = r.__scopedSkipInfo;
|
|
2491
|
-
if (info && info.needsSnap) {
|
|
2492
|
-
const snapHere = facts.__scopedSnapshot || null;
|
|
2493
|
-
const lvlHere =
|
|
2494
|
-
(facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
|
|
2495
|
-
if (!snapHere) continue;
|
|
2496
|
-
if (lvlHere < info.requiredLevel) continue;
|
|
2497
|
-
}
|
|
2840
|
+
const total = candidates.exactLen + candidates.wildLen;
|
|
2841
|
+
for (let ci = 0; ci < total; ci++) {
|
|
2842
|
+
const entry = ci < candidates.exactLen ? candidates.exact[ci] : candidates.wild[ci - candidates.exactLen];
|
|
2843
|
+
const r = entry.rule;
|
|
2844
|
+
if (__skipForwardRuleNow(r)) continue;
|
|
2498
2845
|
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
// solution we pick. In that case, we only need *one* proof of the body, and once all head
|
|
2502
|
-
// triples are already known we can skip proving the body entirely.
|
|
2503
|
-
const headIsStrictGround = r.__headIsStrictGround;
|
|
2846
|
+
const s = unifyTriple(entry.goal, fact, {});
|
|
2847
|
+
if (s === null) continue;
|
|
2504
2848
|
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2849
|
+
const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
|
|
2850
|
+
if (outcome.rulesChanged) {
|
|
2851
|
+
agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
|
|
2852
|
+
agendaCursor = 0;
|
|
2853
|
+
}
|
|
2854
|
+
if (outcome.changedHere) {
|
|
2855
|
+
changed = true;
|
|
2856
|
+
anyChange = true;
|
|
2512
2857
|
}
|
|
2513
|
-
if (allKnown) continue;
|
|
2514
2858
|
}
|
|
2859
|
+
}
|
|
2515
2860
|
|
|
2861
|
+
for (let i = 0; i < forwardRules.length; i++) {
|
|
2862
|
+
const r = forwardRules[i];
|
|
2863
|
+
if (agendaIndex.indexed.has(r)) continue;
|
|
2864
|
+
if (__skipForwardRuleNow(r)) continue;
|
|
2865
|
+
|
|
2866
|
+
const headIsStrictGround = r.__headIsStrictGround;
|
|
2516
2867
|
const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
|
|
2517
2868
|
// Enable builtin deferral / goal reordering for forward rules only.
|
|
2518
2869
|
// This keeps forward-chaining conjunctions order-insensitive while
|
|
@@ -2529,145 +2880,22 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
2529
2880
|
}
|
|
2530
2881
|
|
|
2531
2882
|
for (const s of sols) {
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
// Support "dynamic" rule heads where the consequent is a term that
|
|
2538
|
-
// (after substitution) evaluates to a quoted formula.
|
|
2539
|
-
// Example: { :a :b ?C } => ?C.
|
|
2540
|
-
let dynamicHeadTriples = null;
|
|
2541
|
-
let headBlankLabelsHere = r.headBlankLabels;
|
|
2542
|
-
if (r.__dynamicConclusionTerm) {
|
|
2543
|
-
const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
|
|
2544
|
-
|
|
2545
|
-
// Allow dynamic fuses: ... => ?X. where ?X becomes false
|
|
2546
|
-
if (dynTerm instanceof Literal && dynTerm.value === 'false') {
|
|
2547
|
-
console.log('# Inference fuse triggered: dynamic head resolved to false.');
|
|
2548
|
-
process.exit(2);
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
const dynTriples = __graphTriplesOrTrue(dynTerm);
|
|
2552
|
-
dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
|
|
2553
|
-
|
|
2554
|
-
// If the dynamic head contains explicit blank nodes, treat them as
|
|
2555
|
-
// head blanks for skolemization.
|
|
2556
|
-
const dynHeadBlankLabels =
|
|
2557
|
-
dynamicHeadTriples && dynamicHeadTriples.length
|
|
2558
|
-
? collectBlankLabelsInTriples(dynamicHeadTriples)
|
|
2559
|
-
: null;
|
|
2560
|
-
if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
|
|
2561
|
-
headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
|
|
2562
|
-
}
|
|
2883
|
+
const outcome = __emitForwardRuleSolution(r, i, s);
|
|
2884
|
+
if (outcome.rulesChanged) {
|
|
2885
|
+
agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
|
|
2886
|
+
agendaCursor = 0;
|
|
2563
2887
|
}
|
|
2564
|
-
|
|
2565
|
-
const headPatterns =
|
|
2566
|
-
dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
|
|
2567
|
-
|
|
2568
|
-
for (const cpat of headPatterns) {
|
|
2569
|
-
const instantiated = applySubstTriple(cpat, s);
|
|
2570
|
-
|
|
2571
|
-
const subj = instantiated.s;
|
|
2572
|
-
const obj = instantiated.o;
|
|
2573
|
-
|
|
2574
|
-
const subjIsGraph = subj instanceof GraphTerm;
|
|
2575
|
-
const objIsGraph = obj instanceof GraphTerm;
|
|
2576
|
-
const subjIsTrue = subj instanceof Literal && subj.value === 'true';
|
|
2577
|
-
const objIsTrue = obj instanceof Literal && obj.value === 'true';
|
|
2578
|
-
|
|
2579
|
-
const isFwRuleTriple =
|
|
2580
|
-
isLogImplies(instantiated.p) &&
|
|
2581
|
-
((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
|
|
2582
|
-
|
|
2583
|
-
const isBwRuleTriple =
|
|
2584
|
-
isLogImpliedBy(instantiated.p) &&
|
|
2585
|
-
((subjIsGraph && objIsGraph) || (subjIsGraph && objIsTrue) || (subjIsTrue && objIsGraph));
|
|
2586
|
-
|
|
2587
|
-
if (isFwRuleTriple || isBwRuleTriple) {
|
|
2588
|
-
if (!hasFactIndexed(facts, instantiated)) {
|
|
2589
|
-
factList.push(instantiated);
|
|
2590
|
-
pushFactIndexed(facts, instantiated);
|
|
2591
|
-
const df = new DerivedFact(instantiated, r, instantiatedPremises.slice(), { ...s });
|
|
2592
|
-
derivedForward.push(df);
|
|
2593
|
-
if (typeof onDerived === 'function') onDerived(df);
|
|
2594
|
-
|
|
2595
|
-
changed = true;
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
// Promote rule-producing triples to live rules, treating literal true as {}.
|
|
2599
|
-
const left = __graphTriplesOrTrue(subj);
|
|
2600
|
-
const right = __graphTriplesOrTrue(obj);
|
|
2601
|
-
|
|
2602
|
-
if (left !== null && right !== null) {
|
|
2603
|
-
if (isFwRuleTriple) {
|
|
2604
|
-
const [premise, conclusion] = liftBlankRuleVars(left, right);
|
|
2605
|
-
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
2606
|
-
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
2607
|
-
__prepareForwardRule(newRule);
|
|
2608
|
-
|
|
2609
|
-
const key = __ruleKey(
|
|
2610
|
-
newRule.isForward,
|
|
2611
|
-
newRule.isFuse,
|
|
2612
|
-
newRule.premise,
|
|
2613
|
-
newRule.conclusion,
|
|
2614
|
-
newRule.__dynamicConclusionTerm || null,
|
|
2615
|
-
);
|
|
2616
|
-
if (!forwardRules.__ruleKeySet.has(key)) {
|
|
2617
|
-
forwardRules.__ruleKeySet.add(key);
|
|
2618
|
-
forwardRules.push(newRule);
|
|
2619
|
-
}
|
|
2620
|
-
} else if (isBwRuleTriple) {
|
|
2621
|
-
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
2622
|
-
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
2623
|
-
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
2624
|
-
|
|
2625
|
-
const key = __ruleKey(
|
|
2626
|
-
newRule.isForward,
|
|
2627
|
-
newRule.isFuse,
|
|
2628
|
-
newRule.premise,
|
|
2629
|
-
newRule.conclusion,
|
|
2630
|
-
newRule.__dynamicConclusionTerm || null,
|
|
2631
|
-
);
|
|
2632
|
-
if (!backRules.__ruleKeySet.has(key)) {
|
|
2633
|
-
backRules.__ruleKeySet.add(key);
|
|
2634
|
-
backRules.push(newRule);
|
|
2635
|
-
indexBackRule(backRules, newRule);
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
continue; // skip normal fact handling
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
// Only skolemize blank nodes that occur explicitly in the rule head
|
|
2644
|
-
const inst = skolemizeTripleForHeadBlanks(
|
|
2645
|
-
instantiated,
|
|
2646
|
-
headBlankLabelsHere,
|
|
2647
|
-
skMap,
|
|
2648
|
-
skCounter,
|
|
2649
|
-
fireKey,
|
|
2650
|
-
headSkolemCache,
|
|
2651
|
-
);
|
|
2652
|
-
|
|
2653
|
-
if (!isGroundTriple(inst)) continue;
|
|
2654
|
-
if (hasFactIndexed(facts, inst)) continue;
|
|
2655
|
-
|
|
2656
|
-
factList.push(inst);
|
|
2657
|
-
pushFactIndexed(facts, inst);
|
|
2658
|
-
const df = new DerivedFact(inst, r, instantiatedPremises.slice(), {
|
|
2659
|
-
...s,
|
|
2660
|
-
});
|
|
2661
|
-
derivedForward.push(df);
|
|
2662
|
-
if (typeof onDerived === 'function') onDerived(df);
|
|
2663
|
-
|
|
2888
|
+
if (outcome.changedHere) {
|
|
2664
2889
|
changed = true;
|
|
2890
|
+
anyChange = true;
|
|
2665
2891
|
}
|
|
2666
2892
|
}
|
|
2667
2893
|
}
|
|
2668
2894
|
|
|
2669
|
-
if (!changed)
|
|
2670
|
-
|
|
2895
|
+
if (!changed) {
|
|
2896
|
+
if (agendaCursor < facts.length && agendaIndex.size) continue;
|
|
2897
|
+
break;
|
|
2898
|
+
}
|
|
2671
2899
|
}
|
|
2672
2900
|
|
|
2673
2901
|
return anyChange;
|
|
@@ -2711,7 +2939,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
2711
2939
|
|
|
2712
2940
|
return derivedForward;
|
|
2713
2941
|
} finally {
|
|
2714
|
-
|
|
2942
|
+
exitReasoningRun();
|
|
2715
2943
|
}
|
|
2716
2944
|
}
|
|
2717
2945
|
|
|
@@ -2783,7 +3011,7 @@ function __withScopedSnapshotForQueries(facts, fn) {
|
|
|
2783
3011
|
}
|
|
2784
3012
|
}
|
|
2785
3013
|
|
|
2786
|
-
function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
3014
|
+
function collectLogQueryConclusions(logQueryRules, facts, backRules, opts = {}) {
|
|
2787
3015
|
const queryTriples = [];
|
|
2788
3016
|
const queryDerived = [];
|
|
2789
3017
|
const seen = new Set();
|
|
@@ -2799,6 +3027,8 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
|
2799
3027
|
__attachGoalTable(facts, goalTable);
|
|
2800
3028
|
__attachGoalTable(backRules, goalTable);
|
|
2801
3029
|
|
|
3030
|
+
const captureExplanations = !(opts && opts.captureExplanations === false);
|
|
3031
|
+
|
|
2802
3032
|
// Shared state across all query firings (mirrors forwardChain()).
|
|
2803
3033
|
const varGen = [0];
|
|
2804
3034
|
const skCounter = [0];
|
|
@@ -2850,7 +3080,7 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
|
2850
3080
|
if (seen.has(k)) continue;
|
|
2851
3081
|
seen.add(k);
|
|
2852
3082
|
queryTriples.push(inst);
|
|
2853
|
-
queryDerived.push(
|
|
3083
|
+
queryDerived.push(makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations));
|
|
2854
3084
|
}
|
|
2855
3085
|
}
|
|
2856
3086
|
}
|
|
@@ -2859,16 +3089,23 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
|
2859
3089
|
});
|
|
2860
3090
|
}
|
|
2861
3091
|
|
|
2862
|
-
function forwardChainAndCollectLogQueryConclusions(
|
|
2863
|
-
|
|
3092
|
+
function forwardChainAndCollectLogQueryConclusions(
|
|
3093
|
+
facts,
|
|
3094
|
+
forwardRules,
|
|
3095
|
+
backRules,
|
|
3096
|
+
logQueryRules,
|
|
3097
|
+
onDerived,
|
|
3098
|
+
opts = {},
|
|
3099
|
+
) {
|
|
3100
|
+
enterReasoningRun();
|
|
2864
3101
|
try {
|
|
2865
3102
|
// Forward chain first (saturates `facts`).
|
|
2866
|
-
const derived = forwardChain(facts, forwardRules, backRules, onDerived);
|
|
3103
|
+
const derived = forwardChain(facts, forwardRules, backRules, onDerived, opts);
|
|
2867
3104
|
// Then collect query conclusions against the saturated closure.
|
|
2868
|
-
const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules);
|
|
3105
|
+
const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules, opts);
|
|
2869
3106
|
return { derived, queryTriples, queryDerived };
|
|
2870
3107
|
} finally {
|
|
2871
|
-
|
|
3108
|
+
exitReasoningRun();
|
|
2872
3109
|
}
|
|
2873
3110
|
}
|
|
2874
3111
|
|