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/eyeling.js
CHANGED
|
@@ -4055,10 +4055,12 @@ function main() {
|
|
|
4055
4055
|
// collect log:outputString triples from the instantiated query conclusions.
|
|
4056
4056
|
let outTriples;
|
|
4057
4057
|
if (hasQueries) {
|
|
4058
|
-
const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules
|
|
4058
|
+
const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, null, {
|
|
4059
|
+
captureExplanations: engine.getProofCommentsEnabled(),
|
|
4060
|
+
});
|
|
4059
4061
|
outTriples = res.queryTriples;
|
|
4060
4062
|
} else {
|
|
4061
|
-
engine.forwardChain(facts, frules, brules);
|
|
4063
|
+
engine.forwardChain(facts, frules, brules, null, { captureExplanations: false });
|
|
4062
4064
|
outTriples = facts;
|
|
4063
4065
|
}
|
|
4064
4066
|
|
|
@@ -4169,15 +4171,21 @@ function main() {
|
|
|
4169
4171
|
}
|
|
4170
4172
|
if (entries.length) console.log();
|
|
4171
4173
|
|
|
4172
|
-
engine.forwardChain(
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4174
|
+
engine.forwardChain(
|
|
4175
|
+
facts,
|
|
4176
|
+
frules,
|
|
4177
|
+
brules,
|
|
4178
|
+
(df) => {
|
|
4179
|
+
if (engine.getProofCommentsEnabled()) {
|
|
4180
|
+
engine.printExplanation(df, outPrefixes);
|
|
4181
|
+
console.log(engine.tripleToN3(df.fact, outPrefixes));
|
|
4182
|
+
console.log();
|
|
4183
|
+
} else {
|
|
4184
|
+
console.log(engine.tripleToN3(df.fact, outPrefixes));
|
|
4185
|
+
}
|
|
4186
|
+
},
|
|
4187
|
+
{ captureExplanations: engine.getProofCommentsEnabled() },
|
|
4188
|
+
);
|
|
4181
4189
|
return;
|
|
4182
4190
|
}
|
|
4183
4191
|
|
|
@@ -4197,7 +4205,9 @@ function main() {
|
|
|
4197
4205
|
outTriples = res.queryTriples;
|
|
4198
4206
|
outDerived = res.queryDerived;
|
|
4199
4207
|
} else {
|
|
4200
|
-
derived = engine.forwardChain(facts, frules, brules
|
|
4208
|
+
derived = engine.forwardChain(facts, frules, brules, null, {
|
|
4209
|
+
captureExplanations: engine.getProofCommentsEnabled(),
|
|
4210
|
+
});
|
|
4201
4211
|
outDerived = derived;
|
|
4202
4212
|
outTriples = derived.map((df) => df.fact);
|
|
4203
4213
|
}
|
|
@@ -4726,7 +4736,7 @@ const {
|
|
|
4726
4736
|
// In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
|
|
4727
4737
|
// Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
|
|
4728
4738
|
const RDF_NIL_IRI = RDF_NS + 'nil';
|
|
4729
|
-
const
|
|
4739
|
+
const EMPTY_LIST_TERM = new ListTerm([]);
|
|
4730
4740
|
|
|
4731
4741
|
const { lex, N3SyntaxError } = require('./lexer');
|
|
4732
4742
|
const { Parser } = require('./parser');
|
|
@@ -4768,13 +4778,13 @@ let version = 'dev';
|
|
|
4768
4778
|
try {
|
|
4769
4779
|
// Node: keep package.json version if available
|
|
4770
4780
|
if (typeof require === 'function') version = require('./package.json').version || version;
|
|
4771
|
-
} catch
|
|
4781
|
+
} catch {}
|
|
4772
4782
|
|
|
4773
4783
|
let nodeCrypto = null;
|
|
4774
4784
|
try {
|
|
4775
4785
|
// Node: crypto available
|
|
4776
4786
|
if (typeof require === 'function') nodeCrypto = require('crypto');
|
|
4777
|
-
} catch
|
|
4787
|
+
} catch {}
|
|
4778
4788
|
// For a single reasoning run, this maps a canonical representation
|
|
4779
4789
|
// of the subject term in log:skolem to a Skolem IRI.
|
|
4780
4790
|
const skolemCache = new Map();
|
|
@@ -4786,10 +4796,10 @@ const skolemCache = new Map();
|
|
|
4786
4796
|
// - Across reasoning runs (default): same subject -> different Skolem IRI.
|
|
4787
4797
|
// - Optional legacy mode: stable across runs (CLI: --deterministic-skolem).
|
|
4788
4798
|
let deterministicSkolemAcrossRuns = false;
|
|
4789
|
-
let
|
|
4790
|
-
let
|
|
4799
|
+
let skolemRunDepth = 0;
|
|
4800
|
+
let skolemRunSalt = null;
|
|
4791
4801
|
|
|
4792
|
-
function
|
|
4802
|
+
function makeSkolemRunSalt() {
|
|
4793
4803
|
// Prefer WebCrypto if present (browser/worker)
|
|
4794
4804
|
try {
|
|
4795
4805
|
const g = typeof globalThis !== 'undefined' ? globalThis : null;
|
|
@@ -4803,7 +4813,7 @@ function __makeSkolemRunSalt() {
|
|
|
4803
4813
|
.join('');
|
|
4804
4814
|
}
|
|
4805
4815
|
}
|
|
4806
|
-
} catch
|
|
4816
|
+
} catch {}
|
|
4807
4817
|
|
|
4808
4818
|
// Node.js crypto
|
|
4809
4819
|
try {
|
|
@@ -4811,7 +4821,7 @@ function __makeSkolemRunSalt() {
|
|
|
4811
4821
|
if (typeof nodeCrypto.randomUUID === 'function') return nodeCrypto.randomUUID();
|
|
4812
4822
|
if (typeof nodeCrypto.randomBytes === 'function') return nodeCrypto.randomBytes(16).toString('hex');
|
|
4813
4823
|
}
|
|
4814
|
-
} catch
|
|
4824
|
+
} catch {}
|
|
4815
4825
|
|
|
4816
4826
|
// Last-resort fallback (not cryptographically strong)
|
|
4817
4827
|
return (
|
|
@@ -4819,30 +4829,30 @@ function __makeSkolemRunSalt() {
|
|
|
4819
4829
|
);
|
|
4820
4830
|
}
|
|
4821
4831
|
|
|
4822
|
-
function
|
|
4823
|
-
|
|
4824
|
-
if (
|
|
4832
|
+
function enterReasoningRun() {
|
|
4833
|
+
skolemRunDepth += 1;
|
|
4834
|
+
if (skolemRunDepth === 1) {
|
|
4825
4835
|
skolemCache.clear();
|
|
4826
|
-
|
|
4836
|
+
skolemRunSalt = deterministicSkolemAcrossRuns ? '' : makeSkolemRunSalt();
|
|
4827
4837
|
}
|
|
4828
4838
|
}
|
|
4829
4839
|
|
|
4830
|
-
function
|
|
4831
|
-
if (
|
|
4832
|
-
if (
|
|
4840
|
+
function exitReasoningRun() {
|
|
4841
|
+
if (skolemRunDepth > 0) skolemRunDepth -= 1;
|
|
4842
|
+
if (skolemRunDepth === 0) {
|
|
4833
4843
|
// Clear the salt so a future top-level run gets a fresh one (default mode).
|
|
4834
|
-
|
|
4844
|
+
skolemRunSalt = null;
|
|
4835
4845
|
}
|
|
4836
4846
|
}
|
|
4837
4847
|
|
|
4838
|
-
function
|
|
4848
|
+
function skolemIdForKey(key) {
|
|
4839
4849
|
if (deterministicSkolemAcrossRuns) return deterministicSkolemIdFromKey(key);
|
|
4840
4850
|
// Ensure we have a run salt even if log:skolem is invoked outside forwardChain().
|
|
4841
|
-
if (
|
|
4851
|
+
if (skolemRunSalt === null) {
|
|
4842
4852
|
skolemCache.clear();
|
|
4843
|
-
|
|
4853
|
+
skolemRunSalt = makeSkolemRunSalt();
|
|
4844
4854
|
}
|
|
4845
|
-
return deterministicSkolemIdFromKey(
|
|
4855
|
+
return deterministicSkolemIdFromKey(skolemRunSalt + '|' + key);
|
|
4846
4856
|
}
|
|
4847
4857
|
|
|
4848
4858
|
function getDeterministicSkolemEnabled() {
|
|
@@ -4852,8 +4862,8 @@ function getDeterministicSkolemEnabled() {
|
|
|
4852
4862
|
function setDeterministicSkolemEnabled(v) {
|
|
4853
4863
|
deterministicSkolemAcrossRuns = !!v;
|
|
4854
4864
|
// Reset per-run state so the new mode takes effect immediately for the next run.
|
|
4855
|
-
if (
|
|
4856
|
-
|
|
4865
|
+
if (skolemRunDepth === 0) {
|
|
4866
|
+
skolemRunSalt = null;
|
|
4857
4867
|
skolemCache.clear();
|
|
4858
4868
|
}
|
|
4859
4869
|
}
|
|
@@ -4939,6 +4949,7 @@ function __computeHeadIsStrictGround(r) {
|
|
|
4939
4949
|
if (r.isFuse) return false;
|
|
4940
4950
|
// Dynamic heads depend on runtime bindings; treat as non-ground.
|
|
4941
4951
|
if (r.__dynamicConclusionTerm) return false;
|
|
4952
|
+
if (r.__fromRulePromotion) return false;
|
|
4942
4953
|
if (r.headBlankLabels && r.headBlankLabels.size) return false;
|
|
4943
4954
|
for (const tr of r.conclusion) if (!__isStrictGroundTriple(tr)) return false;
|
|
4944
4955
|
return true;
|
|
@@ -5814,10 +5825,13 @@ function candidateFacts(facts, goal) {
|
|
|
5814
5825
|
else if (wildPO) wild = wildPO;
|
|
5815
5826
|
else wild = facts.__wildPred.length ? facts.__wildPred : null;
|
|
5816
5827
|
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5828
|
+
return {
|
|
5829
|
+
exact: exact || null,
|
|
5830
|
+
wild: wild || null,
|
|
5831
|
+
exactLen: exact ? exact.length : 0,
|
|
5832
|
+
wildLen: wild ? wild.length : 0,
|
|
5833
|
+
totalLen: (exact ? exact.length : 0) + (wild ? wild.length : 0),
|
|
5834
|
+
};
|
|
5821
5835
|
}
|
|
5822
5836
|
|
|
5823
5837
|
return null;
|
|
@@ -5860,6 +5874,11 @@ function pushFactIndexed(facts, tr) {
|
|
|
5860
5874
|
indexFact(facts, tr, idx);
|
|
5861
5875
|
}
|
|
5862
5876
|
|
|
5877
|
+
function makeDerivedRecord(fact, rule, premises, subst, captureExplanations) {
|
|
5878
|
+
if (captureExplanations === false) return { fact };
|
|
5879
|
+
return new DerivedFact(fact, rule, premises.slice(), { ...subst });
|
|
5880
|
+
}
|
|
5881
|
+
|
|
5863
5882
|
function ensureBackRuleIndexes(backRules) {
|
|
5864
5883
|
if (backRules.__byHeadPred && backRules.__wildHeadPred) return;
|
|
5865
5884
|
|
|
@@ -5893,6 +5912,164 @@ function indexBackRule(backRules, r) {
|
|
|
5893
5912
|
}
|
|
5894
5913
|
}
|
|
5895
5914
|
|
|
5915
|
+
function isSinglePremiseAgendaRuleSafe(r, backRules) {
|
|
5916
|
+
if (!r || r.isFuse || !Array.isArray(r.premise) || r.premise.length !== 1) return false;
|
|
5917
|
+
|
|
5918
|
+
// Keep agenda firing restricted to rules whose observable output order is
|
|
5919
|
+
// already stable in the legacy engine. Dynamic heads and head-blank
|
|
5920
|
+
// skolemization are deliberately left on the old path so example outputs keep
|
|
5921
|
+
// the same derived blank labels and rule-promotion behavior.
|
|
5922
|
+
if (r.__dynamicConclusionTerm) return false;
|
|
5923
|
+
if (r.__fromRulePromotion) return false;
|
|
5924
|
+
if (r.headBlankLabels && r.headBlankLabels.size) return false;
|
|
5925
|
+
|
|
5926
|
+
const goal = r.premise[0];
|
|
5927
|
+
|
|
5928
|
+
// Builtin-only bodies need the normal proveGoals path because they can
|
|
5929
|
+
// succeed without matching an extensional fact and may depend on scoped state.
|
|
5930
|
+
if (isBuiltinPred(goal.p)) return false;
|
|
5931
|
+
|
|
5932
|
+
// Safe only when the sole premise cannot be satisfied via backward rules.
|
|
5933
|
+
// Otherwise matching just against newly-seen facts would be incomplete.
|
|
5934
|
+
ensureBackRuleIndexes(backRules);
|
|
5935
|
+
if (goal.p instanceof Iri) {
|
|
5936
|
+
if ((backRules.__byHeadPred.get(goal.p.__tid) || []).length) return false;
|
|
5937
|
+
if (backRules.__wildHeadPred.length) return false;
|
|
5938
|
+
return true;
|
|
5939
|
+
}
|
|
5940
|
+
|
|
5941
|
+
return backRules.__wildHeadPred.length === 0;
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
function mergeSinglePremiseAgendaBuckets() {
|
|
5945
|
+
let out = null;
|
|
5946
|
+
let seen = null;
|
|
5947
|
+
|
|
5948
|
+
for (let i = 0; i < arguments.length; i++) {
|
|
5949
|
+
const bucket = arguments[i];
|
|
5950
|
+
if (!bucket || bucket.length === 0) continue;
|
|
5951
|
+
|
|
5952
|
+
if (out === null) {
|
|
5953
|
+
out = bucket.length === 1 ? [bucket[0]] : bucket.slice();
|
|
5954
|
+
if (bucket.length > 1) seen = new Set(out);
|
|
5955
|
+
continue;
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
if (!seen) seen = new Set(out);
|
|
5959
|
+
for (let j = 0; j < bucket.length; j++) {
|
|
5960
|
+
const entry = bucket[j];
|
|
5961
|
+
if (seen.has(entry)) continue;
|
|
5962
|
+
seen.add(entry);
|
|
5963
|
+
out.push(entry);
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
return out;
|
|
5968
|
+
}
|
|
5969
|
+
|
|
5970
|
+
function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
|
|
5971
|
+
const index = {
|
|
5972
|
+
byPred: new Map(),
|
|
5973
|
+
byPS: new Map(),
|
|
5974
|
+
byPO: new Map(),
|
|
5975
|
+
wildPred: [],
|
|
5976
|
+
wildPS: new Map(),
|
|
5977
|
+
wildPO: new Map(),
|
|
5978
|
+
indexed: new Set(),
|
|
5979
|
+
size: 0,
|
|
5980
|
+
};
|
|
5981
|
+
|
|
5982
|
+
function addToMapArray(m, k, v) {
|
|
5983
|
+
let bucket = m.get(k);
|
|
5984
|
+
if (!bucket) {
|
|
5985
|
+
bucket = [];
|
|
5986
|
+
m.set(k, bucket);
|
|
5987
|
+
}
|
|
5988
|
+
bucket.push(v);
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5991
|
+
for (let i = 0; i < forwardRules.length; i++) {
|
|
5992
|
+
const r = forwardRules[i];
|
|
5993
|
+
if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
|
|
5994
|
+
|
|
5995
|
+
const goal = r.premise[0];
|
|
5996
|
+
const entry = {
|
|
5997
|
+
rule: r,
|
|
5998
|
+
ruleIndex: i,
|
|
5999
|
+
goal,
|
|
6000
|
+
goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
|
|
6001
|
+
goalSKey: termFastKey(goal.s),
|
|
6002
|
+
goalOKey: termFastKey(goal.o),
|
|
6003
|
+
};
|
|
6004
|
+
|
|
6005
|
+
index.indexed.add(r);
|
|
6006
|
+
index.size += 1;
|
|
6007
|
+
|
|
6008
|
+
if (entry.goalPredTid !== null) {
|
|
6009
|
+
if (entry.goalSKey === null && entry.goalOKey === null) addToMapArray(index.byPred, entry.goalPredTid, entry);
|
|
6010
|
+
if (entry.goalSKey !== null) {
|
|
6011
|
+
let ps = index.byPS.get(entry.goalPredTid);
|
|
6012
|
+
if (!ps) {
|
|
6013
|
+
ps = new Map();
|
|
6014
|
+
index.byPS.set(entry.goalPredTid, ps);
|
|
6015
|
+
}
|
|
6016
|
+
addToMapArray(ps, entry.goalSKey, entry);
|
|
6017
|
+
}
|
|
6018
|
+
if (entry.goalOKey !== null) {
|
|
6019
|
+
let po = index.byPO.get(entry.goalPredTid);
|
|
6020
|
+
if (!po) {
|
|
6021
|
+
po = new Map();
|
|
6022
|
+
index.byPO.set(entry.goalPredTid, po);
|
|
6023
|
+
}
|
|
6024
|
+
addToMapArray(po, entry.goalOKey, entry);
|
|
6025
|
+
}
|
|
6026
|
+
} else {
|
|
6027
|
+
if (entry.goalSKey === null && entry.goalOKey === null) index.wildPred.push(entry);
|
|
6028
|
+
if (entry.goalSKey !== null) addToMapArray(index.wildPS, entry.goalSKey, entry);
|
|
6029
|
+
if (entry.goalOKey !== null) addToMapArray(index.wildPO, entry.goalOKey, entry);
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
|
|
6033
|
+
return index;
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
function getSinglePremiseAgendaCandidates(index, fact) {
|
|
6037
|
+
if (!index || index.size === 0) return null;
|
|
6038
|
+
|
|
6039
|
+
const sk = termFastKey(fact.s);
|
|
6040
|
+
const ok = termFastKey(fact.o);
|
|
6041
|
+
|
|
6042
|
+
let exact = null;
|
|
6043
|
+
if (fact.p instanceof Iri) {
|
|
6044
|
+
const pk = fact.p.__tid;
|
|
6045
|
+
const byPred = index.byPred.get(pk) || null;
|
|
6046
|
+
let byPS = null;
|
|
6047
|
+
if (sk !== null) {
|
|
6048
|
+
const ps = index.byPS.get(pk);
|
|
6049
|
+
if (ps) byPS = ps.get(sk) || null;
|
|
6050
|
+
}
|
|
6051
|
+
let byPO = null;
|
|
6052
|
+
if (ok !== null) {
|
|
6053
|
+
const po = index.byPO.get(pk);
|
|
6054
|
+
if (po) byPO = po.get(ok) || null;
|
|
6055
|
+
}
|
|
6056
|
+
|
|
6057
|
+
exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
const wildPred = index.wildPred.length ? index.wildPred : null;
|
|
6061
|
+
let wildPS = null;
|
|
6062
|
+
if (sk !== null) wildPS = index.wildPS.get(sk) || null;
|
|
6063
|
+
|
|
6064
|
+
let wildPO = null;
|
|
6065
|
+
if (ok !== null) wildPO = index.wildPO.get(ok) || null;
|
|
6066
|
+
|
|
6067
|
+
const wild = mergeSinglePremiseAgendaBuckets(wildPred, wildPS, wildPO);
|
|
6068
|
+
|
|
6069
|
+
if (!exact && !wild) return null;
|
|
6070
|
+
return { exact, wild, exactLen: exact ? exact.length : 0, wildLen: wild ? wild.length : 0 };
|
|
6071
|
+
}
|
|
6072
|
+
|
|
5896
6073
|
// ===========================================================================
|
|
5897
6074
|
// Special predicate helpers
|
|
5898
6075
|
// ===========================================================================
|
|
@@ -5918,7 +6095,7 @@ function isLogImpliedBy(p) {
|
|
|
5918
6095
|
// So this improves reuse across repeated backward proofs without changing the
|
|
5919
6096
|
// semantics of recursive goals.
|
|
5920
6097
|
|
|
5921
|
-
function
|
|
6098
|
+
function goalTableScopeVersion(facts, backRules) {
|
|
5922
6099
|
const factCount = Array.isArray(facts) ? facts.length : 0;
|
|
5923
6100
|
const backRuleCount = Array.isArray(backRules) ? backRules.length : 0;
|
|
5924
6101
|
const scopedLevel = facts && typeof facts.__scopedClosureLevel === 'number' ? facts.__scopedClosureLevel : 0;
|
|
@@ -5935,26 +6112,26 @@ function __makeGoalTable() {
|
|
|
5935
6112
|
|
|
5936
6113
|
function __attachGoalTable(scopeCarrier, goalTable) {
|
|
5937
6114
|
if (!scopeCarrier) return goalTable;
|
|
5938
|
-
if (!hasOwn.call(scopeCarrier, '
|
|
5939
|
-
Object.defineProperty(scopeCarrier, '
|
|
6115
|
+
if (!hasOwn.call(scopeCarrier, 'goalTable')) {
|
|
6116
|
+
Object.defineProperty(scopeCarrier, 'goalTable', {
|
|
5940
6117
|
value: goalTable,
|
|
5941
6118
|
enumerable: false,
|
|
5942
6119
|
writable: true,
|
|
5943
6120
|
configurable: true,
|
|
5944
6121
|
});
|
|
5945
6122
|
} else {
|
|
5946
|
-
scopeCarrier.
|
|
6123
|
+
scopeCarrier.goalTable = goalTable;
|
|
5947
6124
|
}
|
|
5948
6125
|
return goalTable;
|
|
5949
6126
|
}
|
|
5950
6127
|
|
|
5951
6128
|
function __ensureGoalTable(facts, backRules) {
|
|
5952
|
-
let table = (facts && facts.
|
|
6129
|
+
let table = (facts && facts.goalTable) || (backRules && backRules.goalTable) || null;
|
|
5953
6130
|
if (!table) table = __makeGoalTable();
|
|
5954
6131
|
__attachGoalTable(facts, table);
|
|
5955
6132
|
__attachGoalTable(backRules, table);
|
|
5956
6133
|
|
|
5957
|
-
const version =
|
|
6134
|
+
const version = goalTableScopeVersion(facts, backRules);
|
|
5958
6135
|
if (table.scopeVersion !== version) {
|
|
5959
6136
|
table.scopeVersion = version;
|
|
5960
6137
|
table.entries.clear();
|
|
@@ -6000,6 +6177,7 @@ function __canStoreGoalMemo(visited, maxResults) {
|
|
|
6000
6177
|
// ===========================================================================
|
|
6001
6178
|
|
|
6002
6179
|
function containsVarTerm(t, v) {
|
|
6180
|
+
if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return false;
|
|
6003
6181
|
if (t instanceof Var) return t.name === v;
|
|
6004
6182
|
if (t instanceof ListTerm) return t.elems.some((e) => containsVarTerm(e, v));
|
|
6005
6183
|
if (t instanceof OpenListTerm) return t.prefix.some((e) => containsVarTerm(e, v)) || t.tailVar === v;
|
|
@@ -6023,6 +6201,7 @@ function isGroundTripleInGraph(tr) {
|
|
|
6023
6201
|
}
|
|
6024
6202
|
|
|
6025
6203
|
function isGroundTerm(t) {
|
|
6204
|
+
if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return true;
|
|
6026
6205
|
if (t instanceof Var) return false;
|
|
6027
6206
|
if (t instanceof ListTerm) return t.elems.every((e) => isGroundTerm(e));
|
|
6028
6207
|
if (t instanceof OpenListTerm) return false;
|
|
@@ -6056,7 +6235,7 @@ function skolemIriFromGroundTerm(t) {
|
|
|
6056
6235
|
const key = skolemKeyFromTerm(t);
|
|
6057
6236
|
let iri = skolemCache.get(key);
|
|
6058
6237
|
if (!iri) {
|
|
6059
|
-
const id =
|
|
6238
|
+
const id = skolemIdForKey(key);
|
|
6060
6239
|
iri = internIri(SKOLEM_NS + id);
|
|
6061
6240
|
skolemCache.set(key, iri);
|
|
6062
6241
|
}
|
|
@@ -6064,6 +6243,9 @@ function skolemIriFromGroundTerm(t) {
|
|
|
6064
6243
|
}
|
|
6065
6244
|
|
|
6066
6245
|
function applySubstTerm(t, s) {
|
|
6246
|
+
// Hot fast path: most terms are already-ground atomic terms.
|
|
6247
|
+
if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return t;
|
|
6248
|
+
|
|
6067
6249
|
// Common case: variable
|
|
6068
6250
|
if (t instanceof Var) {
|
|
6069
6251
|
const first = s[t.name];
|
|
@@ -6258,8 +6440,8 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
6258
6440
|
|
|
6259
6441
|
// Normalize rdf:nil IRI to the empty list term, so it unifies with () and
|
|
6260
6442
|
// list builtins treat it consistently.
|
|
6261
|
-
if (a instanceof Iri && a.value === RDF_NIL_IRI) a =
|
|
6262
|
-
if (b instanceof Iri && b.value === RDF_NIL_IRI) b =
|
|
6443
|
+
if (a instanceof Iri && a.value === RDF_NIL_IRI) a = EMPTY_LIST_TERM;
|
|
6444
|
+
if (b instanceof Iri && b.value === RDF_NIL_IRI) b = EMPTY_LIST_TERM;
|
|
6263
6445
|
|
|
6264
6446
|
// Variable binding
|
|
6265
6447
|
if (a instanceof Var) {
|
|
@@ -6489,10 +6671,10 @@ function __builtinIsSatisfiableWhenFullyUnbound(pIriVal) {
|
|
|
6489
6671
|
}
|
|
6490
6672
|
|
|
6491
6673
|
function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxResults, opts) {
|
|
6492
|
-
const
|
|
6493
|
-
const
|
|
6494
|
-
if (
|
|
6495
|
-
const cached =
|
|
6674
|
+
const goalTable = __canLookupGoalMemo(visited) ? __ensureGoalTable(facts, backRules) : null;
|
|
6675
|
+
const goalMemoKeyNow = goalTable ? __goalMemoKey(goals, subst, facts, opts) : null;
|
|
6676
|
+
if (goalTable && goalTable.entries.has(goalMemoKeyNow)) {
|
|
6677
|
+
const cached = goalTable.entries.get(goalMemoKeyNow) || [];
|
|
6496
6678
|
const cloned = __cloneGoalSolutions(cached);
|
|
6497
6679
|
if (typeof maxResults === 'number' && maxResults > 0 && cloned.length > maxResults)
|
|
6498
6680
|
return cloned.slice(0, maxResults);
|
|
@@ -6511,7 +6693,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6511
6693
|
|
|
6512
6694
|
// IMPORTANT: Goal reordering / deferral is only enabled when explicitly
|
|
6513
6695
|
// requested by the caller (used for forward rules).
|
|
6514
|
-
const
|
|
6696
|
+
const allowDeferredBuiltins = !!(opts && opts.deferBuiltins);
|
|
6515
6697
|
|
|
6516
6698
|
const initialGoals = Array.isArray(goals) ? goals.slice() : [];
|
|
6517
6699
|
const substMut = subst ? { ...subst } : {};
|
|
@@ -6526,8 +6708,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6526
6708
|
|
|
6527
6709
|
if (!initialGoals.length) {
|
|
6528
6710
|
results.push(gcCompactForGoals(substMut, [], answerVars));
|
|
6529
|
-
if (
|
|
6530
|
-
|
|
6711
|
+
if (goalTable && __canStoreGoalMemo(visited, maxResults)) {
|
|
6712
|
+
goalTable.entries.set(goalMemoKeyNow, __cloneGoalSolutions(results));
|
|
6531
6713
|
}
|
|
6532
6714
|
return results;
|
|
6533
6715
|
}
|
|
@@ -6567,14 +6749,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6567
6749
|
const visitedCounts = new Map(); // key -> count
|
|
6568
6750
|
const visitedTrail = []; // stack of keys in insertion order
|
|
6569
6751
|
|
|
6570
|
-
const
|
|
6752
|
+
const termKeyCache = typeof WeakMap === 'function' ? new WeakMap() : null;
|
|
6571
6753
|
|
|
6572
|
-
function
|
|
6754
|
+
function termKeyForVisited(t) {
|
|
6573
6755
|
if (t instanceof Iri && t.value === RDF_NIL_IRI) return '()';
|
|
6574
6756
|
if (t instanceof ListTerm && t.elems.length === 0) return '()';
|
|
6575
6757
|
|
|
6576
|
-
if (
|
|
6577
|
-
const cached =
|
|
6758
|
+
if (termKeyCache && t && typeof t === 'object') {
|
|
6759
|
+
const cached = termKeyCache.get(t);
|
|
6578
6760
|
if (cached) return cached;
|
|
6579
6761
|
}
|
|
6580
6762
|
|
|
@@ -6605,14 +6787,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6605
6787
|
// Iri / Blank and other atomic interned terms
|
|
6606
6788
|
out = 'T' + t.__tid;
|
|
6607
6789
|
} else if (t instanceof ListTerm) {
|
|
6608
|
-
out = '[' + t.elems.map(
|
|
6790
|
+
out = '[' + t.elems.map(termKeyForVisited).join(',') + ']';
|
|
6609
6791
|
} else if (t instanceof OpenListTerm) {
|
|
6610
|
-
out = '[open:' + t.prefix.map(
|
|
6792
|
+
out = '[open:' + t.prefix.map(termKeyForVisited).join(',') + '|tail:' + t.tailVar + ']';
|
|
6611
6793
|
} else if (t instanceof GraphTerm) {
|
|
6612
6794
|
out =
|
|
6613
6795
|
'{' +
|
|
6614
6796
|
t.triples
|
|
6615
|
-
.map((tr) =>
|
|
6797
|
+
.map((tr) => termKeyForVisited(tr.s) + ' ' + termKeyForVisited(tr.p) + ' ' + termKeyForVisited(tr.o))
|
|
6616
6798
|
.join(';') +
|
|
6617
6799
|
'}';
|
|
6618
6800
|
} else {
|
|
@@ -6620,20 +6802,20 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6620
6802
|
out = skolemKeyFromTerm(t);
|
|
6621
6803
|
}
|
|
6622
6804
|
|
|
6623
|
-
if (
|
|
6805
|
+
if (termKeyCache && t && typeof t === 'object') termKeyCache.set(t, out);
|
|
6624
6806
|
return out;
|
|
6625
6807
|
}
|
|
6626
6808
|
|
|
6627
|
-
function
|
|
6628
|
-
return
|
|
6809
|
+
function tripleKeyForVisited(tr) {
|
|
6810
|
+
return termKeyForVisited(tr.s) + '\t' + termKeyForVisited(tr.p) + '\t' + termKeyForVisited(tr.o);
|
|
6629
6811
|
}
|
|
6630
6812
|
|
|
6631
|
-
function
|
|
6813
|
+
function pushVisitedKey(key) {
|
|
6632
6814
|
visitedTrail.push(key);
|
|
6633
6815
|
visitedCounts.set(key, (visitedCounts.get(key) || 0) + 1);
|
|
6634
6816
|
}
|
|
6635
6817
|
|
|
6636
|
-
function
|
|
6818
|
+
function undoVisitedKeysTo(mark) {
|
|
6637
6819
|
for (let i = visitedTrail.length - 1; i >= mark; i--) {
|
|
6638
6820
|
const k = visitedTrail[i];
|
|
6639
6821
|
const c = visitedCounts.get(k);
|
|
@@ -6643,7 +6825,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6643
6825
|
visitedTrail.length = mark;
|
|
6644
6826
|
}
|
|
6645
6827
|
|
|
6646
|
-
for (const tr of initialVisited)
|
|
6828
|
+
for (const tr of initialVisited) pushVisitedKey(tripleKeyForVisited(tr));
|
|
6647
6829
|
|
|
6648
6830
|
// ---------------------------------------------------------------------------
|
|
6649
6831
|
// In-place unification into the mutable substitution + trail.
|
|
@@ -6675,8 +6857,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6675
6857
|
|
|
6676
6858
|
// Normalize rdf:nil IRI to the empty list term, so it unifies with () and
|
|
6677
6859
|
// list builtins treat it consistently.
|
|
6678
|
-
if (a instanceof Iri && a.value === RDF_NIL_IRI) a =
|
|
6679
|
-
if (b instanceof Iri && b.value === RDF_NIL_IRI) b =
|
|
6860
|
+
if (a instanceof Iri && a.value === RDF_NIL_IRI) a = EMPTY_LIST_TERM;
|
|
6861
|
+
if (b instanceof Iri && b.value === RDF_NIL_IRI) b = EMPTY_LIST_TERM;
|
|
6862
|
+
|
|
6863
|
+
if (a === b) return true;
|
|
6680
6864
|
|
|
6681
6865
|
// Variable binding
|
|
6682
6866
|
if (a instanceof Var) return bindVarTrail(a.name, b);
|
|
@@ -6795,7 +6979,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6795
6979
|
kind: 'node',
|
|
6796
6980
|
goalsNow: initialGoals,
|
|
6797
6981
|
curDepth: depth || 0,
|
|
6798
|
-
canDeferBuiltins:
|
|
6982
|
+
canDeferBuiltins: allowDeferredBuiltins,
|
|
6799
6983
|
deferCount: 0,
|
|
6800
6984
|
});
|
|
6801
6985
|
|
|
@@ -6804,7 +6988,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6804
6988
|
|
|
6805
6989
|
if (frame.kind === 'undo') {
|
|
6806
6990
|
undoTo(frame.substMark);
|
|
6807
|
-
|
|
6991
|
+
undoVisitedKeysTo(frame.visitedMark);
|
|
6808
6992
|
continue;
|
|
6809
6993
|
}
|
|
6810
6994
|
|
|
@@ -6863,15 +7047,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6863
7047
|
// still preventing trivial non-termination in mutually recursive rule
|
|
6864
7048
|
// cycles.
|
|
6865
7049
|
if (frame.goalWasVisited && rStd.premise && rStd.premise.length) {
|
|
6866
|
-
let
|
|
7050
|
+
let hasCycle = false;
|
|
6867
7051
|
for (let i = 0; i < rStd.premise.length; i++) {
|
|
6868
|
-
const premKey =
|
|
7052
|
+
const premKey = tripleKeyForVisited(applySubstTriple(rStd.premise[i], substMut));
|
|
6869
7053
|
if (visitedCounts.has(premKey)) {
|
|
6870
|
-
|
|
7054
|
+
hasCycle = true;
|
|
6871
7055
|
break;
|
|
6872
7056
|
}
|
|
6873
7057
|
}
|
|
6874
|
-
if (
|
|
7058
|
+
if (hasCycle) {
|
|
6875
7059
|
undoTo(mark);
|
|
6876
7060
|
continue;
|
|
6877
7061
|
}
|
|
@@ -6880,7 +7064,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6880
7064
|
const newGoals = rStd.premise.concat(frame.restGoals);
|
|
6881
7065
|
|
|
6882
7066
|
const vMark = visitedTrail.length;
|
|
6883
|
-
|
|
7067
|
+
pushVisitedKey(frame.goalKey);
|
|
6884
7068
|
|
|
6885
7069
|
// Explore the rule body; then undo; then resume trying further rules.
|
|
6886
7070
|
stack.push(frame);
|
|
@@ -6902,8 +7086,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6902
7086
|
const candidates = frame.candidates;
|
|
6903
7087
|
const isIndexed = !!candidates;
|
|
6904
7088
|
|
|
6905
|
-
while (frame.idx < (isIndexed ? candidates.
|
|
6906
|
-
|
|
7089
|
+
while (frame.idx < (isIndexed ? candidates.totalLen : factsList.length) && results.length < max) {
|
|
7090
|
+
let f;
|
|
7091
|
+
if (isIndexed) {
|
|
7092
|
+
const idxNow = frame.idx++;
|
|
7093
|
+
if (idxNow < candidates.exactLen) f = factsList[candidates.exact[idxNow]];
|
|
7094
|
+
else f = factsList[candidates.wild[idxNow - candidates.exactLen]];
|
|
7095
|
+
} else {
|
|
7096
|
+
f = factsList[frame.idx++];
|
|
7097
|
+
}
|
|
6907
7098
|
|
|
6908
7099
|
const mark = trail.length;
|
|
6909
7100
|
if (!unifyTripleTrail(frame.goal0, f)) {
|
|
@@ -6946,13 +7137,13 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6946
7137
|
const goal0 = applySubstTriple(rawGoal, substMut);
|
|
6947
7138
|
|
|
6948
7139
|
// 1) Builtins
|
|
6949
|
-
const
|
|
6950
|
-
const
|
|
6951
|
-
const
|
|
7140
|
+
const goalPredicateIri = goal0.p instanceof Iri ? goal0.p.value : null;
|
|
7141
|
+
const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
|
|
7142
|
+
const shouldTreatAsBuiltin =
|
|
6952
7143
|
isBuiltinPred(goal0.p) &&
|
|
6953
|
-
!(
|
|
7144
|
+
!(isRdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
|
|
6954
7145
|
|
|
6955
|
-
if (
|
|
7146
|
+
if (shouldTreatAsBuiltin) {
|
|
6956
7147
|
const remaining = max - results.length;
|
|
6957
7148
|
if (remaining <= 0) continue;
|
|
6958
7149
|
const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
|
|
@@ -6960,11 +7151,11 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6960
7151
|
let deltas = evalBuiltin(goal0, {}, facts, backRules, frame.curDepth, varGen, builtinMax);
|
|
6961
7152
|
|
|
6962
7153
|
const dc = typeof frame.deferCount === 'number' ? frame.deferCount : 0;
|
|
6963
|
-
const
|
|
7154
|
+
const builtinDeltasAreVacuous = deltas.length > 0 && deltas.every((d) => Object.keys(d).length === 0);
|
|
6964
7155
|
|
|
6965
7156
|
if (
|
|
6966
7157
|
frame.canDeferBuiltins &&
|
|
6967
|
-
(!deltas.length ||
|
|
7158
|
+
(!deltas.length || builtinDeltasAreVacuous) &&
|
|
6968
7159
|
restGoals.length &&
|
|
6969
7160
|
__tripleHasVarOrBlank(goal0) &&
|
|
6970
7161
|
dc < goalsNow.length
|
|
@@ -6980,14 +7171,14 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6980
7171
|
continue;
|
|
6981
7172
|
}
|
|
6982
7173
|
|
|
6983
|
-
const
|
|
7174
|
+
const subjectAndObjectAreFullyUnbound =
|
|
6984
7175
|
(goal0.s instanceof Var || goal0.s instanceof Blank) && (goal0.o instanceof Var || goal0.o instanceof Blank);
|
|
6985
7176
|
|
|
6986
7177
|
if (
|
|
6987
7178
|
frame.canDeferBuiltins &&
|
|
6988
7179
|
!deltas.length &&
|
|
6989
|
-
__builtinIsSatisfiableWhenFullyUnbound(
|
|
6990
|
-
|
|
7180
|
+
__builtinIsSatisfiableWhenFullyUnbound(goalPredicateIri) &&
|
|
7181
|
+
subjectAndObjectAreFullyUnbound &&
|
|
6991
7182
|
(!restGoals.length || dc >= goalsNow.length)
|
|
6992
7183
|
) {
|
|
6993
7184
|
deltas = [{}];
|
|
@@ -7022,7 +7213,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
7022
7213
|
// We therefore *allow* re-entering a visited goal, but when a goal is
|
|
7023
7214
|
// already visited we avoid applying backward rules whose premises would
|
|
7024
7215
|
// immediately re-enter any visited goal again (a cheap cycle guard).
|
|
7025
|
-
const goalKey =
|
|
7216
|
+
const goalKey = tripleKeyForVisited(goal0);
|
|
7026
7217
|
const goalWasVisited = visitedCounts.has(goalKey);
|
|
7027
7218
|
|
|
7028
7219
|
// 3) Backward rules (indexed by head predicate) — explored first
|
|
@@ -7072,8 +7263,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
7072
7263
|
}
|
|
7073
7264
|
}
|
|
7074
7265
|
|
|
7075
|
-
if (
|
|
7076
|
-
|
|
7266
|
+
if (goalTable && __canStoreGoalMemo(visited, maxResults)) {
|
|
7267
|
+
goalTable.entries.set(goalMemoKeyNow, __cloneGoalSolutions(results));
|
|
7077
7268
|
}
|
|
7078
7269
|
|
|
7079
7270
|
return results;
|
|
@@ -7083,8 +7274,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
7083
7274
|
// Forward chaining to fixpoint
|
|
7084
7275
|
// ===========================================================================
|
|
7085
7276
|
|
|
7086
|
-
function forwardChain(facts, forwardRules, backRules, onDerived /* optional
|
|
7087
|
-
|
|
7277
|
+
function forwardChain(facts, forwardRules, backRules, onDerived /* optional */, opts = {}) {
|
|
7278
|
+
enterReasoningRun();
|
|
7088
7279
|
try {
|
|
7089
7280
|
ensureFactIndexes(facts);
|
|
7090
7281
|
ensureBackRuleIndexes(backRules);
|
|
@@ -7093,7 +7284,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
7093
7284
|
__attachGoalTable(facts, goalTable);
|
|
7094
7285
|
__attachGoalTable(backRules, goalTable);
|
|
7095
7286
|
|
|
7096
|
-
const
|
|
7287
|
+
const captureExplanations = !(opts && opts.captureExplanations === false);
|
|
7097
7288
|
const derivedForward = [];
|
|
7098
7289
|
const varGen = [0];
|
|
7099
7290
|
const skCounter = [0];
|
|
@@ -7169,46 +7360,216 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
7169
7360
|
return snap;
|
|
7170
7361
|
}
|
|
7171
7362
|
|
|
7363
|
+
function __skipForwardRuleNow(r) {
|
|
7364
|
+
// Skip forward rules that are guaranteed to "delay" due to scoped
|
|
7365
|
+
// builtins (log:collectAllIn / log:forAllIn / log:includes / log:notIncludes)
|
|
7366
|
+
// until a snapshot exists (and a certain closure level is reached).
|
|
7367
|
+
// This prevents expensive proofs that will definitely fail in Phase A
|
|
7368
|
+
// and in early closure levels.
|
|
7369
|
+
const info = r.__scopedSkipInfo;
|
|
7370
|
+
if (info && info.needsSnap) {
|
|
7371
|
+
const snapHere = facts.__scopedSnapshot || null;
|
|
7372
|
+
const lvlHere = (facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
|
|
7373
|
+
if (!snapHere) return true;
|
|
7374
|
+
if (lvlHere < info.requiredLevel) return true;
|
|
7375
|
+
}
|
|
7376
|
+
|
|
7377
|
+
// Optimization: if the rule head is **structurally ground** (no vars anywhere, even inside
|
|
7378
|
+
// quoted formulas) and has no head blanks, then the head does not depend on which body
|
|
7379
|
+
// solution we pick. In that case, we only need *one* proof of the body, and once all head
|
|
7380
|
+
// triples are already known we can skip proving the body entirely.
|
|
7381
|
+
const headIsStrictGround = r.__headIsStrictGround;
|
|
7382
|
+
if (headIsStrictGround) {
|
|
7383
|
+
let allKnown = true;
|
|
7384
|
+
for (const tr of r.conclusion) {
|
|
7385
|
+
if (!hasFactIndexed(facts, tr)) {
|
|
7386
|
+
allKnown = false;
|
|
7387
|
+
break;
|
|
7388
|
+
}
|
|
7389
|
+
}
|
|
7390
|
+
if (allKnown) return true;
|
|
7391
|
+
}
|
|
7392
|
+
|
|
7393
|
+
return false;
|
|
7394
|
+
}
|
|
7395
|
+
|
|
7396
|
+
function __emitForwardRuleSolution(r, ruleIndex, s) {
|
|
7397
|
+
let changedHere = false;
|
|
7398
|
+
let rulesChanged = false;
|
|
7399
|
+
|
|
7400
|
+
// IMPORTANT: one skolem map per *rule firing*
|
|
7401
|
+
const skMap = {};
|
|
7402
|
+
const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
|
|
7403
|
+
const fireKey = __firingKey(ruleIndex, instantiatedPremises);
|
|
7404
|
+
|
|
7405
|
+
// Support "dynamic" rule heads where the consequent is a term that
|
|
7406
|
+
// (after substitution) evaluates to a quoted formula.
|
|
7407
|
+
// Example: { :a :b ?C } => ?C.
|
|
7408
|
+
let dynamicHeadTriples = null;
|
|
7409
|
+
let headBlankLabelsHere = r.headBlankLabels;
|
|
7410
|
+
if (r.__dynamicConclusionTerm) {
|
|
7411
|
+
const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
|
|
7412
|
+
|
|
7413
|
+
// Allow dynamic fuses: ... => ?X. where ?X becomes false
|
|
7414
|
+
if (dynTerm instanceof Literal && dynTerm.value === 'false') {
|
|
7415
|
+
console.log('# Inference fuse triggered: dynamic head resolved to false.');
|
|
7416
|
+
process.exit(2);
|
|
7417
|
+
}
|
|
7418
|
+
|
|
7419
|
+
const dynTriples = __graphTriplesOrTrue(dynTerm);
|
|
7420
|
+
dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
|
|
7421
|
+
|
|
7422
|
+
// If the dynamic head contains explicit blank nodes, treat them as
|
|
7423
|
+
// head blanks for skolemization.
|
|
7424
|
+
const dynHeadBlankLabels =
|
|
7425
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? collectBlankLabelsInTriples(dynamicHeadTriples) : null;
|
|
7426
|
+
if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
|
|
7427
|
+
headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7431
|
+
const headPatterns =
|
|
7432
|
+
dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
|
|
7433
|
+
|
|
7434
|
+
for (const cpat of headPatterns) {
|
|
7435
|
+
const instantiated = applySubstTriple(cpat, s);
|
|
7436
|
+
|
|
7437
|
+
const subj = instantiated.s;
|
|
7438
|
+
const obj = instantiated.o;
|
|
7439
|
+
|
|
7440
|
+
const subjIsGraph = subj instanceof GraphTerm;
|
|
7441
|
+
const objIsGraph = obj instanceof GraphTerm;
|
|
7442
|
+
const subjIsTrue = subj instanceof Literal && subj.value === 'true';
|
|
7443
|
+
const objIsTrue = obj instanceof Literal && obj.value === 'true';
|
|
7444
|
+
|
|
7445
|
+
const isFwRuleTriple =
|
|
7446
|
+
isLogImplies(instantiated.p) &&
|
|
7447
|
+
((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
|
|
7448
|
+
|
|
7449
|
+
const isBwRuleTriple =
|
|
7450
|
+
isLogImpliedBy(instantiated.p) &&
|
|
7451
|
+
((subjIsGraph && objIsGraph) || (subjIsGraph && objIsTrue) || (subjIsTrue && objIsGraph));
|
|
7452
|
+
|
|
7453
|
+
if (isFwRuleTriple || isBwRuleTriple) {
|
|
7454
|
+
if (!hasFactIndexed(facts, instantiated)) {
|
|
7455
|
+
pushFactIndexed(facts, instantiated);
|
|
7456
|
+
const df = makeDerivedRecord(instantiated, r, instantiatedPremises, s, captureExplanations);
|
|
7457
|
+
derivedForward.push(df);
|
|
7458
|
+
if (typeof onDerived === 'function') onDerived(df);
|
|
7459
|
+
changedHere = true;
|
|
7460
|
+
}
|
|
7461
|
+
|
|
7462
|
+
// Promote rule-producing triples to live rules, treating literal true as {}.
|
|
7463
|
+
const left = __graphTriplesOrTrue(subj);
|
|
7464
|
+
const right = __graphTriplesOrTrue(obj);
|
|
7465
|
+
|
|
7466
|
+
if (left !== null && right !== null) {
|
|
7467
|
+
if (isFwRuleTriple) {
|
|
7468
|
+
const [premise, conclusion] = liftBlankRuleVars(left, right);
|
|
7469
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
7470
|
+
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
7471
|
+
__prepareForwardRule(newRule);
|
|
7472
|
+
|
|
7473
|
+
const key = __ruleKey(
|
|
7474
|
+
newRule.isForward,
|
|
7475
|
+
newRule.isFuse,
|
|
7476
|
+
newRule.premise,
|
|
7477
|
+
newRule.conclusion,
|
|
7478
|
+
newRule.__dynamicConclusionTerm || null,
|
|
7479
|
+
);
|
|
7480
|
+
if (!forwardRules.__ruleKeySet.has(key)) {
|
|
7481
|
+
forwardRules.__ruleKeySet.add(key);
|
|
7482
|
+
forwardRules.push(newRule);
|
|
7483
|
+
rulesChanged = true;
|
|
7484
|
+
}
|
|
7485
|
+
} else if (isBwRuleTriple) {
|
|
7486
|
+
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
7487
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
7488
|
+
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
7489
|
+
|
|
7490
|
+
const key = __ruleKey(
|
|
7491
|
+
newRule.isForward,
|
|
7492
|
+
newRule.isFuse,
|
|
7493
|
+
newRule.premise,
|
|
7494
|
+
newRule.conclusion,
|
|
7495
|
+
newRule.__dynamicConclusionTerm || null,
|
|
7496
|
+
);
|
|
7497
|
+
if (!backRules.__ruleKeySet.has(key)) {
|
|
7498
|
+
backRules.__ruleKeySet.add(key);
|
|
7499
|
+
backRules.push(newRule);
|
|
7500
|
+
indexBackRule(backRules, newRule);
|
|
7501
|
+
rulesChanged = true;
|
|
7502
|
+
}
|
|
7503
|
+
}
|
|
7504
|
+
}
|
|
7505
|
+
|
|
7506
|
+
continue; // skip normal fact handling
|
|
7507
|
+
}
|
|
7508
|
+
|
|
7509
|
+
// Only skolemize blank nodes that occur explicitly in the rule head
|
|
7510
|
+
const inst = skolemizeTripleForHeadBlanks(
|
|
7511
|
+
instantiated,
|
|
7512
|
+
headBlankLabelsHere,
|
|
7513
|
+
skMap,
|
|
7514
|
+
skCounter,
|
|
7515
|
+
fireKey,
|
|
7516
|
+
headSkolemCache,
|
|
7517
|
+
);
|
|
7518
|
+
|
|
7519
|
+
if (!isGroundTriple(inst)) continue;
|
|
7520
|
+
if (hasFactIndexed(facts, inst)) continue;
|
|
7521
|
+
|
|
7522
|
+
pushFactIndexed(facts, inst);
|
|
7523
|
+
const df = makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations);
|
|
7524
|
+
derivedForward.push(df);
|
|
7525
|
+
if (typeof onDerived === 'function') onDerived(df);
|
|
7526
|
+
|
|
7527
|
+
changedHere = true;
|
|
7528
|
+
}
|
|
7529
|
+
|
|
7530
|
+
return { changedHere, rulesChanged };
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7172
7533
|
function runFixpoint() {
|
|
7173
7534
|
let anyChange = false;
|
|
7535
|
+
let agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
|
|
7536
|
+
let agendaCursor = 0;
|
|
7174
7537
|
|
|
7175
7538
|
while (true) {
|
|
7176
7539
|
let changed = false;
|
|
7177
7540
|
|
|
7178
|
-
|
|
7179
|
-
const
|
|
7541
|
+
while (agendaCursor < facts.length && agendaIndex.size) {
|
|
7542
|
+
const fact = facts[agendaCursor++];
|
|
7543
|
+
const candidates = getSinglePremiseAgendaCandidates(agendaIndex, fact);
|
|
7544
|
+
if (!candidates) continue;
|
|
7180
7545
|
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
const info = r.__scopedSkipInfo;
|
|
7187
|
-
if (info && info.needsSnap) {
|
|
7188
|
-
const snapHere = facts.__scopedSnapshot || null;
|
|
7189
|
-
const lvlHere =
|
|
7190
|
-
(facts && typeof facts.__scopedClosureLevel === 'number' && facts.__scopedClosureLevel) || 0;
|
|
7191
|
-
if (!snapHere) continue;
|
|
7192
|
-
if (lvlHere < info.requiredLevel) continue;
|
|
7193
|
-
}
|
|
7546
|
+
const total = candidates.exactLen + candidates.wildLen;
|
|
7547
|
+
for (let ci = 0; ci < total; ci++) {
|
|
7548
|
+
const entry = ci < candidates.exactLen ? candidates.exact[ci] : candidates.wild[ci - candidates.exactLen];
|
|
7549
|
+
const r = entry.rule;
|
|
7550
|
+
if (__skipForwardRuleNow(r)) continue;
|
|
7194
7551
|
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
// solution we pick. In that case, we only need *one* proof of the body, and once all head
|
|
7198
|
-
// triples are already known we can skip proving the body entirely.
|
|
7199
|
-
const headIsStrictGround = r.__headIsStrictGround;
|
|
7552
|
+
const s = unifyTriple(entry.goal, fact, {});
|
|
7553
|
+
if (s === null) continue;
|
|
7200
7554
|
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7555
|
+
const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
|
|
7556
|
+
if (outcome.rulesChanged) {
|
|
7557
|
+
agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
|
|
7558
|
+
agendaCursor = 0;
|
|
7559
|
+
}
|
|
7560
|
+
if (outcome.changedHere) {
|
|
7561
|
+
changed = true;
|
|
7562
|
+
anyChange = true;
|
|
7208
7563
|
}
|
|
7209
|
-
if (allKnown) continue;
|
|
7210
7564
|
}
|
|
7565
|
+
}
|
|
7566
|
+
|
|
7567
|
+
for (let i = 0; i < forwardRules.length; i++) {
|
|
7568
|
+
const r = forwardRules[i];
|
|
7569
|
+
if (agendaIndex.indexed.has(r)) continue;
|
|
7570
|
+
if (__skipForwardRuleNow(r)) continue;
|
|
7211
7571
|
|
|
7572
|
+
const headIsStrictGround = r.__headIsStrictGround;
|
|
7212
7573
|
const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
|
|
7213
7574
|
// Enable builtin deferral / goal reordering for forward rules only.
|
|
7214
7575
|
// This keeps forward-chaining conjunctions order-insensitive while
|
|
@@ -7225,145 +7586,22 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
7225
7586
|
}
|
|
7226
7587
|
|
|
7227
7588
|
for (const s of sols) {
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
// Support "dynamic" rule heads where the consequent is a term that
|
|
7234
|
-
// (after substitution) evaluates to a quoted formula.
|
|
7235
|
-
// Example: { :a :b ?C } => ?C.
|
|
7236
|
-
let dynamicHeadTriples = null;
|
|
7237
|
-
let headBlankLabelsHere = r.headBlankLabels;
|
|
7238
|
-
if (r.__dynamicConclusionTerm) {
|
|
7239
|
-
const dynTerm = applySubstTerm(r.__dynamicConclusionTerm, s);
|
|
7240
|
-
|
|
7241
|
-
// Allow dynamic fuses: ... => ?X. where ?X becomes false
|
|
7242
|
-
if (dynTerm instanceof Literal && dynTerm.value === 'false') {
|
|
7243
|
-
console.log('# Inference fuse triggered: dynamic head resolved to false.');
|
|
7244
|
-
process.exit(2);
|
|
7245
|
-
}
|
|
7246
|
-
|
|
7247
|
-
const dynTriples = __graphTriplesOrTrue(dynTerm);
|
|
7248
|
-
dynamicHeadTriples = dynTriples !== null ? dynTriples : [];
|
|
7249
|
-
|
|
7250
|
-
// If the dynamic head contains explicit blank nodes, treat them as
|
|
7251
|
-
// head blanks for skolemization.
|
|
7252
|
-
const dynHeadBlankLabels =
|
|
7253
|
-
dynamicHeadTriples && dynamicHeadTriples.length
|
|
7254
|
-
? collectBlankLabelsInTriples(dynamicHeadTriples)
|
|
7255
|
-
: null;
|
|
7256
|
-
if (dynHeadBlankLabels && dynHeadBlankLabels.size) {
|
|
7257
|
-
headBlankLabelsHere = new Set([...headBlankLabelsHere, ...dynHeadBlankLabels]);
|
|
7258
|
-
}
|
|
7589
|
+
const outcome = __emitForwardRuleSolution(r, i, s);
|
|
7590
|
+
if (outcome.rulesChanged) {
|
|
7591
|
+
agendaIndex = makeSinglePremiseAgendaIndex(forwardRules, backRules);
|
|
7592
|
+
agendaCursor = 0;
|
|
7259
7593
|
}
|
|
7260
|
-
|
|
7261
|
-
const headPatterns =
|
|
7262
|
-
dynamicHeadTriples && dynamicHeadTriples.length ? r.conclusion.concat(dynamicHeadTriples) : r.conclusion;
|
|
7263
|
-
|
|
7264
|
-
for (const cpat of headPatterns) {
|
|
7265
|
-
const instantiated = applySubstTriple(cpat, s);
|
|
7266
|
-
|
|
7267
|
-
const subj = instantiated.s;
|
|
7268
|
-
const obj = instantiated.o;
|
|
7269
|
-
|
|
7270
|
-
const subjIsGraph = subj instanceof GraphTerm;
|
|
7271
|
-
const objIsGraph = obj instanceof GraphTerm;
|
|
7272
|
-
const subjIsTrue = subj instanceof Literal && subj.value === 'true';
|
|
7273
|
-
const objIsTrue = obj instanceof Literal && obj.value === 'true';
|
|
7274
|
-
|
|
7275
|
-
const isFwRuleTriple =
|
|
7276
|
-
isLogImplies(instantiated.p) &&
|
|
7277
|
-
((subjIsGraph && objIsGraph) || (subjIsTrue && objIsGraph) || (subjIsGraph && objIsTrue));
|
|
7278
|
-
|
|
7279
|
-
const isBwRuleTriple =
|
|
7280
|
-
isLogImpliedBy(instantiated.p) &&
|
|
7281
|
-
((subjIsGraph && objIsGraph) || (subjIsGraph && objIsTrue) || (subjIsTrue && objIsGraph));
|
|
7282
|
-
|
|
7283
|
-
if (isFwRuleTriple || isBwRuleTriple) {
|
|
7284
|
-
if (!hasFactIndexed(facts, instantiated)) {
|
|
7285
|
-
factList.push(instantiated);
|
|
7286
|
-
pushFactIndexed(facts, instantiated);
|
|
7287
|
-
const df = new DerivedFact(instantiated, r, instantiatedPremises.slice(), { ...s });
|
|
7288
|
-
derivedForward.push(df);
|
|
7289
|
-
if (typeof onDerived === 'function') onDerived(df);
|
|
7290
|
-
|
|
7291
|
-
changed = true;
|
|
7292
|
-
}
|
|
7293
|
-
|
|
7294
|
-
// Promote rule-producing triples to live rules, treating literal true as {}.
|
|
7295
|
-
const left = __graphTriplesOrTrue(subj);
|
|
7296
|
-
const right = __graphTriplesOrTrue(obj);
|
|
7297
|
-
|
|
7298
|
-
if (left !== null && right !== null) {
|
|
7299
|
-
if (isFwRuleTriple) {
|
|
7300
|
-
const [premise, conclusion] = liftBlankRuleVars(left, right);
|
|
7301
|
-
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
7302
|
-
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
7303
|
-
__prepareForwardRule(newRule);
|
|
7304
|
-
|
|
7305
|
-
const key = __ruleKey(
|
|
7306
|
-
newRule.isForward,
|
|
7307
|
-
newRule.isFuse,
|
|
7308
|
-
newRule.premise,
|
|
7309
|
-
newRule.conclusion,
|
|
7310
|
-
newRule.__dynamicConclusionTerm || null,
|
|
7311
|
-
);
|
|
7312
|
-
if (!forwardRules.__ruleKeySet.has(key)) {
|
|
7313
|
-
forwardRules.__ruleKeySet.add(key);
|
|
7314
|
-
forwardRules.push(newRule);
|
|
7315
|
-
}
|
|
7316
|
-
} else if (isBwRuleTriple) {
|
|
7317
|
-
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
7318
|
-
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
7319
|
-
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
7320
|
-
|
|
7321
|
-
const key = __ruleKey(
|
|
7322
|
-
newRule.isForward,
|
|
7323
|
-
newRule.isFuse,
|
|
7324
|
-
newRule.premise,
|
|
7325
|
-
newRule.conclusion,
|
|
7326
|
-
newRule.__dynamicConclusionTerm || null,
|
|
7327
|
-
);
|
|
7328
|
-
if (!backRules.__ruleKeySet.has(key)) {
|
|
7329
|
-
backRules.__ruleKeySet.add(key);
|
|
7330
|
-
backRules.push(newRule);
|
|
7331
|
-
indexBackRule(backRules, newRule);
|
|
7332
|
-
}
|
|
7333
|
-
}
|
|
7334
|
-
}
|
|
7335
|
-
|
|
7336
|
-
continue; // skip normal fact handling
|
|
7337
|
-
}
|
|
7338
|
-
|
|
7339
|
-
// Only skolemize blank nodes that occur explicitly in the rule head
|
|
7340
|
-
const inst = skolemizeTripleForHeadBlanks(
|
|
7341
|
-
instantiated,
|
|
7342
|
-
headBlankLabelsHere,
|
|
7343
|
-
skMap,
|
|
7344
|
-
skCounter,
|
|
7345
|
-
fireKey,
|
|
7346
|
-
headSkolemCache,
|
|
7347
|
-
);
|
|
7348
|
-
|
|
7349
|
-
if (!isGroundTriple(inst)) continue;
|
|
7350
|
-
if (hasFactIndexed(facts, inst)) continue;
|
|
7351
|
-
|
|
7352
|
-
factList.push(inst);
|
|
7353
|
-
pushFactIndexed(facts, inst);
|
|
7354
|
-
const df = new DerivedFact(inst, r, instantiatedPremises.slice(), {
|
|
7355
|
-
...s,
|
|
7356
|
-
});
|
|
7357
|
-
derivedForward.push(df);
|
|
7358
|
-
if (typeof onDerived === 'function') onDerived(df);
|
|
7359
|
-
|
|
7594
|
+
if (outcome.changedHere) {
|
|
7360
7595
|
changed = true;
|
|
7596
|
+
anyChange = true;
|
|
7361
7597
|
}
|
|
7362
7598
|
}
|
|
7363
7599
|
}
|
|
7364
7600
|
|
|
7365
|
-
if (!changed)
|
|
7366
|
-
|
|
7601
|
+
if (!changed) {
|
|
7602
|
+
if (agendaCursor < facts.length && agendaIndex.size) continue;
|
|
7603
|
+
break;
|
|
7604
|
+
}
|
|
7367
7605
|
}
|
|
7368
7606
|
|
|
7369
7607
|
return anyChange;
|
|
@@ -7407,7 +7645,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
7407
7645
|
|
|
7408
7646
|
return derivedForward;
|
|
7409
7647
|
} finally {
|
|
7410
|
-
|
|
7648
|
+
exitReasoningRun();
|
|
7411
7649
|
}
|
|
7412
7650
|
}
|
|
7413
7651
|
|
|
@@ -7479,7 +7717,7 @@ function __withScopedSnapshotForQueries(facts, fn) {
|
|
|
7479
7717
|
}
|
|
7480
7718
|
}
|
|
7481
7719
|
|
|
7482
|
-
function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
7720
|
+
function collectLogQueryConclusions(logQueryRules, facts, backRules, opts = {}) {
|
|
7483
7721
|
const queryTriples = [];
|
|
7484
7722
|
const queryDerived = [];
|
|
7485
7723
|
const seen = new Set();
|
|
@@ -7495,6 +7733,8 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
|
7495
7733
|
__attachGoalTable(facts, goalTable);
|
|
7496
7734
|
__attachGoalTable(backRules, goalTable);
|
|
7497
7735
|
|
|
7736
|
+
const captureExplanations = !(opts && opts.captureExplanations === false);
|
|
7737
|
+
|
|
7498
7738
|
// Shared state across all query firings (mirrors forwardChain()).
|
|
7499
7739
|
const varGen = [0];
|
|
7500
7740
|
const skCounter = [0];
|
|
@@ -7546,7 +7786,7 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
|
7546
7786
|
if (seen.has(k)) continue;
|
|
7547
7787
|
seen.add(k);
|
|
7548
7788
|
queryTriples.push(inst);
|
|
7549
|
-
queryDerived.push(
|
|
7789
|
+
queryDerived.push(makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations));
|
|
7550
7790
|
}
|
|
7551
7791
|
}
|
|
7552
7792
|
}
|
|
@@ -7555,16 +7795,23 @@ function collectLogQueryConclusions(logQueryRules, facts, backRules) {
|
|
|
7555
7795
|
});
|
|
7556
7796
|
}
|
|
7557
7797
|
|
|
7558
|
-
function forwardChainAndCollectLogQueryConclusions(
|
|
7559
|
-
|
|
7798
|
+
function forwardChainAndCollectLogQueryConclusions(
|
|
7799
|
+
facts,
|
|
7800
|
+
forwardRules,
|
|
7801
|
+
backRules,
|
|
7802
|
+
logQueryRules,
|
|
7803
|
+
onDerived,
|
|
7804
|
+
opts = {},
|
|
7805
|
+
) {
|
|
7806
|
+
enterReasoningRun();
|
|
7560
7807
|
try {
|
|
7561
7808
|
// Forward chain first (saturates `facts`).
|
|
7562
|
-
const derived = forwardChain(facts, forwardRules, backRules, onDerived);
|
|
7809
|
+
const derived = forwardChain(facts, forwardRules, backRules, onDerived, opts);
|
|
7563
7810
|
// Then collect query conclusions against the saturated closure.
|
|
7564
|
-
const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules);
|
|
7811
|
+
const { queryTriples, queryDerived } = collectLogQueryConclusions(logQueryRules, facts, backRules, opts);
|
|
7565
7812
|
return { derived, queryTriples, queryDerived };
|
|
7566
7813
|
} finally {
|
|
7567
|
-
|
|
7814
|
+
exitReasoningRun();
|
|
7568
7815
|
}
|
|
7569
7816
|
}
|
|
7570
7817
|
|