eyeling 1.11.19 → 1.11.21

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 CHANGED
@@ -182,7 +182,7 @@ Eyeling interns IRIs and Literals by string value. Interning is a quiet performa
182
182
 
183
183
  In addition, interned **Iri**/**Literal** terms (and generated **Blank** terms) get a small, non-enumerable integer id `.__tid` that is stable for the lifetime of the process. This `__tid` is used as the engine’s “fast key”:
184
184
 
185
- - fact indexes (`__byPS` / `__byPO`) key by `__tid` instead of building `"I:..."` / `"L:..."` strings
185
+ - fact indexes (`__byPred` / `__byPS` / `__byPO`) key by `__tid` values **and store fact *indices*** (predicate buckets are keyed by `predicate.__tid`, and PS/PO buckets are keyed by the subject/object `.__tid`; buckets contain integer indices into the `facts` array)
186
186
  - duplicate detection uses `"sid pid oid"` where each component is a `__tid`
187
187
  - unification/equality has an early-out when two terms share the same `__tid`
188
188
 
@@ -441,9 +441,9 @@ Facts live in an array `facts: Triple[]`.
441
441
 
442
442
  Eyeling attaches hidden (non-enumerable) index fields:
443
443
 
444
- * `facts.__byPred: Map<predicateIRI, Triple[]>`
445
- * `facts.__byPS: Map<predicateIRI, Map<termId, Triple[]>>` where `termId` is `term.__tid`
446
- * `facts.__byPO: Map<predicateIRI, Map<termId, Triple[]>>` where `termId` is `term.__tid`
444
+ * `facts.__byPred: Map<predicateId, number[]>` where each entry is an index into `facts` (and `predicateId` is `predicate.__tid`)
445
+ * `facts.__byPS: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
446
+ * `facts.__byPO: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
447
447
  * `facts.__keySet: Set<string>` for a fast-path `"sid pid oid"` key (all three are `__tid` values)
448
448
 
449
449
  `termFastKey(term)` returns a `termId` (`term.__tid`) for **Iri**, **Literal**, and **Blank** terms, and `null` for structured terms (lists, quoted graphs) and variables.
@@ -519,9 +519,6 @@ for delta in deltas:
519
519
 
520
520
  **Implementation note (performance):** as of this version, Eyeling also avoids allocating short-lived substitution objects when matching goals against **facts** and when unifying a **backward-rule head** with the current goal. Instead of calling the pure `unifyTriple(..., subst)` (which clones the substitution on each variable bind), the prover performs an **in-place unification** directly into the mutable `substMut` store and records only the newly-bound variable names on the trail. This typically reduces GC pressure significantly on reachability / path-search workloads, where unification is executed extremely frequently.
521
521
 
522
- **Implementation note (performance): ground-goal memoization.** When the current goal triple is fully ground (contains no `Var`, no open-list term, and no quoted formula), satisfying it cannot introduce new bindings. Eyeling maintains a small per-proof memo cache for such ground goals, keyed by a canonical triple key. If the cache records that a ground goal is satisfiable (either by a direct fact match or only via backward rules), the prover skips re-proving it and continues with the remaining goals. This reduces repeated work in programs that issue the same membership-style checks many times (even without full tabling). The cache is bounded (default: 20k entries) and uses an LRU-style eviction; advanced callers can override the bound via `opts.groundMemoMax` passed to `proveGoals`.
523
-
524
-
525
522
 
526
523
  So built-ins behave like relations that can generate zero, one, or many possible bindings. A list generator might yield many deltas; a numeric test yields zero or one.
527
524
 
package/eyeling.js CHANGED
@@ -1020,19 +1020,22 @@ function listAppendSplit(parts, resElems, subst) {
1020
1020
 
1021
1021
  function __rdfListObjectsForSP(facts, predIri, subj) {
1022
1022
  ensureFactIndexes(facts);
1023
+ // Engine indexes predicate buckets by predicate term id.
1024
+ const pk = internIri(predIri).__tid;
1023
1025
  const sk = termFastKey(subj);
1024
1026
  if (sk !== null) {
1025
- const ps = facts.__byPS.get(predIri);
1027
+ const ps = facts.__byPS.get(pk);
1026
1028
  if (ps) {
1027
1029
  const bucket = ps.get(sk);
1028
- if (bucket && bucket.length) return bucket.map((tr) => tr.o);
1030
+ if (bucket && bucket.length) return bucket.map((i) => facts[i].o);
1029
1031
  }
1030
1032
  }
1031
1033
 
1032
1034
  // Fallback scan (covers non-indexable terms)
1033
- const pb = facts.__byPred.get(predIri) || [];
1035
+ const pb = facts.__byPred.get(pk) || [];
1034
1036
  const out = [];
1035
- for (const tr of pb) {
1037
+ for (const i of pb) {
1038
+ const tr = facts[i];
1036
1039
  if (termsEqual(tr.s, subj)) out.push(tr.o);
1037
1040
  }
1038
1041
  return out;
@@ -4953,12 +4956,13 @@ function alphaEqGraphTriples(xs, ys) {
4953
4956
  // ===========================================================================
4954
4957
  //
4955
4958
  // Facts:
4956
- // - __byPred: Map<predicateIRI, Triple[]>
4957
- // - __byPO: Map<predicateIRI, Map<objectKey, Triple[]>>
4958
- // - __keySet: Set<"S\tP\tO"> for IRI/Literal-only triples (fast dup check)
4959
+ // - __byPred: Map<predicateId, number[]> (indices into facts array)
4960
+ // - __byPS: Map<predicateId, Map<subjectId, number[]>>
4961
+ // - __byPO: Map<predicateId, Map<objectId, number[]>>
4962
+ // - __keySet: Set<"S\tP\tO"> for Iri/Literal/Blank-only triples (fast dup check)
4959
4963
  //
4960
4964
  // Backward rules:
4961
- // - __byHeadPred: Map<headPredicateIRI, Rule[]>
4965
+ // - __byHeadPred: Map<headPredicateId, Rule[]>
4962
4966
  // - __wildHeadPred: Rule[] (non-IRI head predicate)
4963
4967
 
4964
4968
  function termFastKey(t) {
@@ -4998,19 +5002,20 @@ function ensureFactIndexes(facts) {
4998
5002
  writable: true,
4999
5003
  });
5000
5004
 
5001
- for (const f of facts) indexFact(facts, f);
5005
+ for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i);
5002
5006
  }
5003
5007
 
5004
- function indexFact(facts, tr) {
5008
+ function indexFact(facts, tr, idx) {
5005
5009
  if (tr.p instanceof Iri) {
5006
- const pk = tr.p.value;
5010
+ // Use predicate term id as the primary key to avoid hashing long IRI strings.
5011
+ const pk = tr.p.__tid;
5007
5012
 
5008
5013
  let pb = facts.__byPred.get(pk);
5009
5014
  if (!pb) {
5010
5015
  pb = [];
5011
5016
  facts.__byPred.set(pk, pb);
5012
5017
  }
5013
- pb.push(tr);
5018
+ pb.push(idx);
5014
5019
 
5015
5020
  const sk = termFastKey(tr.s);
5016
5021
  if (sk !== null) {
@@ -5024,7 +5029,7 @@ function indexFact(facts, tr) {
5024
5029
  psb = [];
5025
5030
  ps.set(sk, psb);
5026
5031
  }
5027
- psb.push(tr);
5032
+ psb.push(idx);
5028
5033
  }
5029
5034
 
5030
5035
  const ok = termFastKey(tr.o);
@@ -5039,7 +5044,7 @@ function indexFact(facts, tr) {
5039
5044
  pob = [];
5040
5045
  po.set(ok, pob);
5041
5046
  }
5042
- pob.push(tr);
5047
+ pob.push(idx);
5043
5048
  }
5044
5049
  }
5045
5050
 
@@ -5051,19 +5056,19 @@ function candidateFacts(facts, goal) {
5051
5056
  ensureFactIndexes(facts);
5052
5057
 
5053
5058
  if (goal.p instanceof Iri) {
5054
- const pk = goal.p.value;
5059
+ const pk = goal.p.__tid;
5055
5060
 
5056
5061
  const sk = termFastKey(goal.s);
5057
5062
  const ok = termFastKey(goal.o);
5058
5063
 
5059
- /** @type {Triple[] | null} */
5064
+ /** @type {number[] | null} */
5060
5065
  let byPS = null;
5061
5066
  if (sk !== null) {
5062
5067
  const ps = facts.__byPS.get(pk);
5063
5068
  if (ps) byPS = ps.get(sk) || null;
5064
5069
  }
5065
5070
 
5066
- /** @type {Triple[] | null} */
5071
+ /** @type {number[] | null} */
5067
5072
  let byPO = null;
5068
5073
  if (ok !== null) {
5069
5074
  const po = facts.__byPO.get(pk);
@@ -5077,7 +5082,7 @@ function candidateFacts(facts, goal) {
5077
5082
  return facts.__byPred.get(pk) || [];
5078
5083
  }
5079
5084
 
5080
- return facts;
5085
+ return null;
5081
5086
  }
5082
5087
 
5083
5088
  function hasFactIndexed(facts, tr) {
@@ -5087,7 +5092,7 @@ function hasFactIndexed(facts, tr) {
5087
5092
  if (key !== null) return facts.__keySet.has(key);
5088
5093
 
5089
5094
  if (tr.p instanceof Iri) {
5090
- const pk = tr.p.value;
5095
+ const pk = tr.p.__tid;
5091
5096
 
5092
5097
  const ok = termFastKey(tr.o);
5093
5098
  if (ok !== null) {
@@ -5098,12 +5103,12 @@ function hasFactIndexed(facts, tr) {
5098
5103
  // different existentials unless explicitly connected. Do NOT treat
5099
5104
  // triples as duplicates modulo blank renaming, or you'll incorrectly
5100
5105
  // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
5101
- return pob.some((t) => triplesEqual(t, tr));
5106
+ return pob.some((i) => triplesEqual(facts[i], tr));
5102
5107
  }
5103
5108
  }
5104
5109
 
5105
5110
  const pb = facts.__byPred.get(pk) || [];
5106
- return pb.some((t) => triplesEqual(t, tr));
5111
+ return pb.some((i) => triplesEqual(facts[i], tr));
5107
5112
  }
5108
5113
 
5109
5114
  // Non-IRI predicate: fall back to strict triple equality.
@@ -5112,8 +5117,9 @@ function hasFactIndexed(facts, tr) {
5112
5117
 
5113
5118
  function pushFactIndexed(facts, tr) {
5114
5119
  ensureFactIndexes(facts);
5120
+ const idx = facts.length;
5115
5121
  facts.push(tr);
5116
- indexFact(facts, tr);
5122
+ indexFact(facts, tr, idx);
5117
5123
  }
5118
5124
 
5119
5125
  function ensureBackRuleIndexes(backRules) {
@@ -5137,7 +5143,7 @@ function indexBackRule(backRules, r) {
5137
5143
  if (!r || !r.conclusion || r.conclusion.length !== 1) return;
5138
5144
  const head = r.conclusion[0];
5139
5145
  if (head && head.p instanceof Iri) {
5140
- const k = head.p.value;
5146
+ const k = head.p.__tid;
5141
5147
  let bucket = backRules.__byHeadPred.get(k);
5142
5148
  if (!bucket) {
5143
5149
  bucket = [];
@@ -5691,61 +5697,6 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
5691
5697
  trail.length = mark;
5692
5698
  }
5693
5699
 
5694
-
5695
- // ---------------------------------------------------------------------------
5696
- // Ground-goal memoization (small cache)
5697
- // ---------------------------------------------------------------------------
5698
- //
5699
- // For fully ground (variable-free) triple subgoals, the proof search can be
5700
- // repeated many times across different branches of the DFS. Because a ground
5701
- // goal never produces bindings, it is safe to memoize whether it is satisfiable.
5702
- //
5703
- // Cache values:
5704
- // 0 = unsatisfiable
5705
- // 1 = satisfiable (direct fact match exists)
5706
- // 2 = satisfiable (only via backward rules)
5707
- // 3 = in progress (cycle breaker)
5708
- const __GROUND_MEMO_MAX =
5709
- opts && typeof opts.groundMemoMax === 'number' && opts.groundMemoMax > 0 ? opts.groundMemoMax : 20000;
5710
- const __groundGoalMemo = new Map();
5711
- const __MEMO_FALSE = 0;
5712
- const __MEMO_TRUE_FACT = 1;
5713
- const __MEMO_TRUE_RULE = 2;
5714
- const __MEMO_INPROGRESS = 3;
5715
-
5716
- function __memoSet(key, val) {
5717
- if (__groundGoalMemo.has(key)) __groundGoalMemo.delete(key);
5718
- __groundGoalMemo.set(key, val);
5719
- if (__groundGoalMemo.size > __GROUND_MEMO_MAX) {
5720
- const firstKey = __groundGoalMemo.keys().next().value;
5721
- __groundGoalMemo.delete(firstKey);
5722
- }
5723
- }
5724
-
5725
- function __isMemoGroundTerm(t) {
5726
- if (t instanceof Var) return false;
5727
- if (t instanceof OpenListTerm) return false;
5728
- if (t instanceof GraphTerm) return false;
5729
- if (t instanceof ListTerm) return t.elems.every(__isMemoGroundTerm);
5730
- // Iri, Literal, Blank (and other atomics) are considered ground here.
5731
- return true;
5732
- }
5733
-
5734
- function __memoKeyTerm(t) {
5735
- if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return t.__tid || null;
5736
- if (t instanceof ListTerm) return 'L(' + t.elems.map(__memoKeyTerm).join(',') + ')';
5737
- return null;
5738
- }
5739
-
5740
- function __memoKeyTriple(tr) {
5741
- if (!__isMemoGroundTerm(tr.s) || !__isMemoGroundTerm(tr.p) || !__isMemoGroundTerm(tr.o)) return null;
5742
- const ks = __memoKeyTerm(tr.s);
5743
- const kp = __memoKeyTerm(tr.p);
5744
- const ko = __memoKeyTerm(tr.o);
5745
- if (ks === null || kp === null || ko === null) return null;
5746
- return ks + ' ' + kp + ' ' + ko;
5747
- }
5748
-
5749
5700
  // In-place unification into the mutable substitution + trail.
5750
5701
  // This avoids allocating short-lived "delta" substitution objects on the hot path
5751
5702
  // (facts and backward-rule head matching).
@@ -5875,163 +5826,6 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
5875
5826
  return true;
5876
5827
  }
5877
5828
 
5878
-
5879
- // Boolean (existence) prover used by ground-goal memoization.
5880
- // Returns true as soon as one proof for the goal list is found.
5881
- function __existsGoals(goalsNow, curDepth, visitedNow) {
5882
- if (!goalsNow.length) return true;
5883
-
5884
- const rawGoal = goalsNow[0];
5885
- const restGoals = goalsNow.length > 1 ? goalsNow.slice(1) : [];
5886
- const goal0 = applySubstTriple(rawGoal, substMut);
5887
-
5888
- // Builtins
5889
- const __pv0 = goal0.p instanceof Iri ? goal0.p.value : null;
5890
- const __rdfFirstOrRest = __pv0 === RDF_NS + 'first' || __pv0 === RDF_NS + 'rest';
5891
- const __treatBuiltin =
5892
- isBuiltinPred(goal0.p) &&
5893
- !(__rdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
5894
-
5895
- if (__treatBuiltin) {
5896
- const deltas = evalBuiltin(goal0, {}, facts, backRules, curDepth, varGen, 1);
5897
- for (const delta of deltas) {
5898
- const mark = trail.length;
5899
- if (!applyDeltaToSubst(delta)) {
5900
- undoTo(mark);
5901
- continue;
5902
- }
5903
- const ok = __existsGoals(restGoals, curDepth + 1, visitedNow);
5904
- undoTo(mark);
5905
- if (ok) return true;
5906
- }
5907
- return false;
5908
- }
5909
-
5910
- // Loop check for backward reasoning
5911
- if (listHasTriple(visitedNow, goal0)) return false;
5912
- const visitedForRules = visitedNow.concat([goal0]);
5913
-
5914
- // Ground-goal memo check (only safe when the goal is fully ground)
5915
- const memoKey = __memoKeyTriple(goal0);
5916
- if (memoKey) {
5917
- const mv = __groundGoalMemo.get(memoKey);
5918
- if (mv === __MEMO_FALSE) return false;
5919
- if (mv === __MEMO_TRUE_FACT) return __existsGoals(restGoals, curDepth + 1, visitedNow);
5920
- if (mv === __MEMO_TRUE_RULE) return __existsGoals(restGoals, curDepth + 1, visitedForRules);
5921
- if (mv === __MEMO_INPROGRESS) return false;
5922
-
5923
- __memoSet(memoKey, __MEMO_INPROGRESS);
5924
- const kind = __groundGoalSatisfiableKind(goal0, curDepth, visitedNow, visitedForRules);
5925
- __memoSet(memoKey, kind);
5926
-
5927
- if (kind === __MEMO_FALSE) return false;
5928
- if (kind === __MEMO_TRUE_FACT) return __existsGoals(restGoals, curDepth + 1, visitedNow);
5929
- return __existsGoals(restGoals, curDepth + 1, visitedForRules);
5930
- }
5931
-
5932
- // Backward rules (indexed by head predicate) — explored first
5933
- if (goal0.p instanceof Iri) {
5934
- ensureBackRuleIndexes(backRules);
5935
- const candRules = (backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
5936
-
5937
- for (const r of candRules) {
5938
- if (r.conclusion.length !== 1) continue;
5939
- const rawHead = r.conclusion[0];
5940
- if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
5941
-
5942
- const rStd = standardizeRule(r, varGen);
5943
- const head = rStd.conclusion[0];
5944
-
5945
- const mark = trail.length;
5946
- if (!unifyTripleTrail(head, goal0)) {
5947
- undoTo(mark);
5948
- continue;
5949
- }
5950
-
5951
- const newGoals = rStd.premise.concat(restGoals);
5952
- const ok = __existsGoals(newGoals, curDepth + 1, visitedForRules);
5953
- undoTo(mark);
5954
- if (ok) return true;
5955
- }
5956
- }
5957
-
5958
- // Facts
5959
- if (goal0.p instanceof Iri) {
5960
- const candidates = candidateFacts(facts, goal0);
5961
- for (const f of candidates) {
5962
- const mark = trail.length;
5963
- if (!unifyTripleTrail(goal0, f)) {
5964
- undoTo(mark);
5965
- continue;
5966
- }
5967
- const ok = __existsGoals(restGoals, curDepth + 1, visitedNow);
5968
- undoTo(mark);
5969
- if (ok) return true;
5970
- }
5971
- return false;
5972
- }
5973
-
5974
- for (const f of facts) {
5975
- const mark = trail.length;
5976
- if (!unifyTripleTrail(goal0, f)) {
5977
- undoTo(mark);
5978
- continue;
5979
- }
5980
- const ok = __existsGoals(restGoals, curDepth + 1, visitedNow);
5981
- undoTo(mark);
5982
- if (ok) return true;
5983
- }
5984
- return false;
5985
- }
5986
-
5987
- function __groundGoalSatisfiableKind(goal0, curDepth, visitedNow, visitedForRules) {
5988
- // Try direct facts first (cheap): if any fact matches, we can treat this as "fact satisfiable"
5989
- // for visited semantics.
5990
- if (goal0.p instanceof Iri) {
5991
- const candidates = candidateFacts(facts, goal0);
5992
- for (const f of candidates) {
5993
- const mark = trail.length;
5994
- const ok = unifyTripleTrail(goal0, f);
5995
- undoTo(mark);
5996
- if (ok) return __MEMO_TRUE_FACT;
5997
- }
5998
- } else {
5999
- for (const f of facts) {
6000
- const mark = trail.length;
6001
- const ok = unifyTripleTrail(goal0, f);
6002
- undoTo(mark);
6003
- if (ok) return __MEMO_TRUE_FACT;
6004
- }
6005
- }
6006
-
6007
- // Otherwise, look for a proof via backward rules.
6008
- if (goal0.p instanceof Iri) {
6009
- ensureBackRuleIndexes(backRules);
6010
- const candRules = (backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
6011
-
6012
- for (const r of candRules) {
6013
- if (r.conclusion.length !== 1) continue;
6014
- const rawHead = r.conclusion[0];
6015
- if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
6016
-
6017
- const rStd = standardizeRule(r, varGen);
6018
- const head = rStd.conclusion[0];
6019
-
6020
- const mark = trail.length;
6021
- if (!unifyTripleTrail(head, goal0)) {
6022
- undoTo(mark);
6023
- continue;
6024
- }
6025
-
6026
- const ok = __existsGoals(rStd.premise, curDepth + 1, visitedForRules);
6027
- undoTo(mark);
6028
- if (ok) return __MEMO_TRUE_RULE;
6029
- }
6030
- }
6031
-
6032
- return __MEMO_FALSE;
6033
- }
6034
-
6035
5829
  function dfs(goalsNow, curDepth, visitedNow, canDeferBuiltins, deferCount) {
6036
5830
  if (results.length >= max) return;
6037
5831
  if (!goalsNow.length) {
@@ -6108,50 +5902,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6108
5902
  if (listHasTriple(visitedNow, goal0)) return;
6109
5903
  const visitedForRules = visitedNow.concat([goal0]);
6110
5904
 
6111
- // Ground-goal memoization: if a goal is fully ground, it cannot introduce new bindings,
6112
- // so we can cache whether it is satisfiable and skip re-proving it across branches.
6113
- const __memoKey0 = __memoKeyTriple(goal0);
6114
- if (__memoKey0) {
6115
- const mv = __groundGoalMemo.get(__memoKey0);
6116
- if (mv === __MEMO_FALSE) return;
6117
-
6118
- if (mv === __MEMO_TRUE_FACT || mv === __MEMO_TRUE_RULE) {
6119
- const __nextVisited = mv === __MEMO_TRUE_FACT ? visitedNow : visitedForRules;
6120
- if (!restGoals.length) {
6121
- results.push(gcCompactForGoals(substMut, [], answerVars));
6122
- } else {
6123
- dfs(restGoals, curDepth + 1, __nextVisited, canDeferBuiltins, 0);
6124
- }
6125
- return;
6126
- }
6127
-
6128
- // Cycle breaker for recursive ground goals.
6129
- if (mv === __MEMO_INPROGRESS) return;
6130
-
6131
- __memoSet(__memoKey0, __MEMO_INPROGRESS);
6132
- const kind = __groundGoalSatisfiableKind(goal0, curDepth, visitedNow, visitedForRules);
6133
- __memoSet(__memoKey0, kind);
6134
-
6135
- if (kind === __MEMO_FALSE) return;
6136
-
6137
- const __nextVisited = kind === __MEMO_TRUE_FACT ? visitedNow : visitedForRules;
6138
- if (!restGoals.length) {
6139
- results.push(gcCompactForGoals(substMut, [], answerVars));
6140
- } else {
6141
- dfs(restGoals, curDepth + 1, __nextVisited, canDeferBuiltins, 0);
6142
- }
6143
- return;
6144
- }
6145
-
6146
5905
  // 3) Backward rules (indexed by head predicate) — explored first
6147
5906
  if (goal0.p instanceof Iri) {
6148
5907
  ensureBackRuleIndexes(backRules);
6149
- const candRules = (backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
5908
+ const candRules = (backRules.__byHeadPred.get(goal0.p.__tid) || []).concat(backRules.__wildHeadPred);
6150
5909
 
6151
5910
  for (const r of candRules) {
6152
5911
  if (r.conclusion.length !== 1) continue;
6153
5912
  const rawHead = r.conclusion[0];
6154
- if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
5913
+ if (rawHead.p instanceof Iri && rawHead.p.__tid !== goal0.p.__tid) continue;
6155
5914
 
6156
5915
  const rStd = standardizeRule(r, varGen);
6157
5916
  const head = rStd.conclusion[0];
@@ -6177,7 +5936,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
6177
5936
  // 4) Try to satisfy the goal from known facts
6178
5937
  if (goal0.p instanceof Iri) {
6179
5938
  const candidates = candidateFacts(facts, goal0);
6180
- for (const f of candidates) {
5939
+ for (const idx of candidates) {
5940
+ const f = facts[idx];
6181
5941
  const mark = trail.length;
6182
5942
  if (!unifyTripleTrail(goal0, f)) {
6183
5943
  undoTo(mark);
package/lib/builtins.js CHANGED
@@ -1008,19 +1008,22 @@ function listAppendSplit(parts, resElems, subst) {
1008
1008
 
1009
1009
  function __rdfListObjectsForSP(facts, predIri, subj) {
1010
1010
  ensureFactIndexes(facts);
1011
+ // Engine indexes predicate buckets by predicate term id.
1012
+ const pk = internIri(predIri).__tid;
1011
1013
  const sk = termFastKey(subj);
1012
1014
  if (sk !== null) {
1013
- const ps = facts.__byPS.get(predIri);
1015
+ const ps = facts.__byPS.get(pk);
1014
1016
  if (ps) {
1015
1017
  const bucket = ps.get(sk);
1016
- if (bucket && bucket.length) return bucket.map((tr) => tr.o);
1018
+ if (bucket && bucket.length) return bucket.map((i) => facts[i].o);
1017
1019
  }
1018
1020
  }
1019
1021
 
1020
1022
  // Fallback scan (covers non-indexable terms)
1021
- const pb = facts.__byPred.get(predIri) || [];
1023
+ const pb = facts.__byPred.get(pk) || [];
1022
1024
  const out = [];
1023
- for (const tr of pb) {
1025
+ for (const i of pb) {
1026
+ const tr = facts[i];
1024
1027
  if (termsEqual(tr.s, subj)) out.push(tr.o);
1025
1028
  }
1026
1029
  return out;
package/lib/engine.js CHANGED
@@ -579,12 +579,13 @@ function alphaEqGraphTriples(xs, ys) {
579
579
  // ===========================================================================
580
580
  //
581
581
  // Facts:
582
- // - __byPred: Map<predicateIRI, Triple[]>
583
- // - __byPO: Map<predicateIRI, Map<objectKey, Triple[]>>
584
- // - __keySet: Set<"S\tP\tO"> for IRI/Literal-only triples (fast dup check)
582
+ // - __byPred: Map<predicateId, number[]> (indices into facts array)
583
+ // - __byPS: Map<predicateId, Map<subjectId, number[]>>
584
+ // - __byPO: Map<predicateId, Map<objectId, number[]>>
585
+ // - __keySet: Set<"S\tP\tO"> for Iri/Literal/Blank-only triples (fast dup check)
585
586
  //
586
587
  // Backward rules:
587
- // - __byHeadPred: Map<headPredicateIRI, Rule[]>
588
+ // - __byHeadPred: Map<headPredicateId, Rule[]>
588
589
  // - __wildHeadPred: Rule[] (non-IRI head predicate)
589
590
 
590
591
  function termFastKey(t) {
@@ -624,19 +625,20 @@ function ensureFactIndexes(facts) {
624
625
  writable: true,
625
626
  });
626
627
 
627
- for (const f of facts) indexFact(facts, f);
628
+ for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i);
628
629
  }
629
630
 
630
- function indexFact(facts, tr) {
631
+ function indexFact(facts, tr, idx) {
631
632
  if (tr.p instanceof Iri) {
632
- const pk = tr.p.value;
633
+ // Use predicate term id as the primary key to avoid hashing long IRI strings.
634
+ const pk = tr.p.__tid;
633
635
 
634
636
  let pb = facts.__byPred.get(pk);
635
637
  if (!pb) {
636
638
  pb = [];
637
639
  facts.__byPred.set(pk, pb);
638
640
  }
639
- pb.push(tr);
641
+ pb.push(idx);
640
642
 
641
643
  const sk = termFastKey(tr.s);
642
644
  if (sk !== null) {
@@ -650,7 +652,7 @@ function indexFact(facts, tr) {
650
652
  psb = [];
651
653
  ps.set(sk, psb);
652
654
  }
653
- psb.push(tr);
655
+ psb.push(idx);
654
656
  }
655
657
 
656
658
  const ok = termFastKey(tr.o);
@@ -665,7 +667,7 @@ function indexFact(facts, tr) {
665
667
  pob = [];
666
668
  po.set(ok, pob);
667
669
  }
668
- pob.push(tr);
670
+ pob.push(idx);
669
671
  }
670
672
  }
671
673
 
@@ -677,19 +679,19 @@ function candidateFacts(facts, goal) {
677
679
  ensureFactIndexes(facts);
678
680
 
679
681
  if (goal.p instanceof Iri) {
680
- const pk = goal.p.value;
682
+ const pk = goal.p.__tid;
681
683
 
682
684
  const sk = termFastKey(goal.s);
683
685
  const ok = termFastKey(goal.o);
684
686
 
685
- /** @type {Triple[] | null} */
687
+ /** @type {number[] | null} */
686
688
  let byPS = null;
687
689
  if (sk !== null) {
688
690
  const ps = facts.__byPS.get(pk);
689
691
  if (ps) byPS = ps.get(sk) || null;
690
692
  }
691
693
 
692
- /** @type {Triple[] | null} */
694
+ /** @type {number[] | null} */
693
695
  let byPO = null;
694
696
  if (ok !== null) {
695
697
  const po = facts.__byPO.get(pk);
@@ -703,7 +705,7 @@ function candidateFacts(facts, goal) {
703
705
  return facts.__byPred.get(pk) || [];
704
706
  }
705
707
 
706
- return facts;
708
+ return null;
707
709
  }
708
710
 
709
711
  function hasFactIndexed(facts, tr) {
@@ -713,7 +715,7 @@ function hasFactIndexed(facts, tr) {
713
715
  if (key !== null) return facts.__keySet.has(key);
714
716
 
715
717
  if (tr.p instanceof Iri) {
716
- const pk = tr.p.value;
718
+ const pk = tr.p.__tid;
717
719
 
718
720
  const ok = termFastKey(tr.o);
719
721
  if (ok !== null) {
@@ -724,12 +726,12 @@ function hasFactIndexed(facts, tr) {
724
726
  // different existentials unless explicitly connected. Do NOT treat
725
727
  // triples as duplicates modulo blank renaming, or you'll incorrectly
726
728
  // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
727
- return pob.some((t) => triplesEqual(t, tr));
729
+ return pob.some((i) => triplesEqual(facts[i], tr));
728
730
  }
729
731
  }
730
732
 
731
733
  const pb = facts.__byPred.get(pk) || [];
732
- return pb.some((t) => triplesEqual(t, tr));
734
+ return pb.some((i) => triplesEqual(facts[i], tr));
733
735
  }
734
736
 
735
737
  // Non-IRI predicate: fall back to strict triple equality.
@@ -738,8 +740,9 @@ function hasFactIndexed(facts, tr) {
738
740
 
739
741
  function pushFactIndexed(facts, tr) {
740
742
  ensureFactIndexes(facts);
743
+ const idx = facts.length;
741
744
  facts.push(tr);
742
- indexFact(facts, tr);
745
+ indexFact(facts, tr, idx);
743
746
  }
744
747
 
745
748
  function ensureBackRuleIndexes(backRules) {
@@ -763,7 +766,7 @@ function indexBackRule(backRules, r) {
763
766
  if (!r || !r.conclusion || r.conclusion.length !== 1) return;
764
767
  const head = r.conclusion[0];
765
768
  if (head && head.p instanceof Iri) {
766
- const k = head.p.value;
769
+ const k = head.p.__tid;
767
770
  let bucket = backRules.__byHeadPred.get(k);
768
771
  if (!bucket) {
769
772
  bucket = [];
@@ -1317,60 +1320,6 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
1317
1320
  trail.length = mark;
1318
1321
  }
1319
1322
 
1320
- // ---------------------------------------------------------------------------
1321
- // Ground-goal memoization (small cache)
1322
- // ---------------------------------------------------------------------------
1323
- //
1324
- // For fully ground (variable-free) triple subgoals, the proof search can be
1325
- // repeated many times across different branches of the DFS. Because a ground
1326
- // goal never produces bindings, it is safe to memoize whether it is satisfiable.
1327
- //
1328
- // Cache values:
1329
- // 0 = unsatisfiable
1330
- // 1 = satisfiable (direct fact match exists)
1331
- // 2 = satisfiable (only via backward rules)
1332
- // 3 = in progress (cycle breaker)
1333
- const __GROUND_MEMO_MAX =
1334
- opts && typeof opts.groundMemoMax === 'number' && opts.groundMemoMax > 0 ? opts.groundMemoMax : 20000;
1335
- const __groundGoalMemo = new Map();
1336
- const __MEMO_FALSE = 0;
1337
- const __MEMO_TRUE_FACT = 1;
1338
- const __MEMO_TRUE_RULE = 2;
1339
- const __MEMO_INPROGRESS = 3;
1340
-
1341
- function __memoSet(key, val) {
1342
- if (__groundGoalMemo.has(key)) __groundGoalMemo.delete(key);
1343
- __groundGoalMemo.set(key, val);
1344
- if (__groundGoalMemo.size > __GROUND_MEMO_MAX) {
1345
- const firstKey = __groundGoalMemo.keys().next().value;
1346
- __groundGoalMemo.delete(firstKey);
1347
- }
1348
- }
1349
-
1350
- function __isMemoGroundTerm(t) {
1351
- if (t instanceof Var) return false;
1352
- if (t instanceof OpenListTerm) return false;
1353
- if (t instanceof GraphTerm) return false;
1354
- if (t instanceof ListTerm) return t.elems.every(__isMemoGroundTerm);
1355
- // Iri, Literal, Blank (and other atomics) are considered ground here.
1356
- return true;
1357
- }
1358
-
1359
- function __memoKeyTerm(t) {
1360
- if (t instanceof Iri || t instanceof Literal || t instanceof Blank) return t.__tid || null;
1361
- if (t instanceof ListTerm) return 'L(' + t.elems.map(__memoKeyTerm).join(',') + ')';
1362
- return null;
1363
- }
1364
-
1365
- function __memoKeyTriple(tr) {
1366
- if (!__isMemoGroundTerm(tr.s) || !__isMemoGroundTerm(tr.p) || !__isMemoGroundTerm(tr.o)) return null;
1367
- const ks = __memoKeyTerm(tr.s);
1368
- const kp = __memoKeyTerm(tr.p);
1369
- const ko = __memoKeyTerm(tr.o);
1370
- if (ks === null || kp === null || ko === null) return null;
1371
- return ks + ' ' + kp + ' ' + ko;
1372
- }
1373
-
1374
1323
  // In-place unification into the mutable substitution + trail.
1375
1324
  // This avoids allocating short-lived "delta" substitution objects on the hot path
1376
1325
  // (facts and backward-rule head matching).
@@ -1500,162 +1449,6 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
1500
1449
  return true;
1501
1450
  }
1502
1451
 
1503
- // Boolean (existence) prover used by ground-goal memoization.
1504
- // Returns true as soon as one proof for the goal list is found.
1505
- function __existsGoals(goalsNow, curDepth, visitedNow) {
1506
- if (!goalsNow.length) return true;
1507
-
1508
- const rawGoal = goalsNow[0];
1509
- const restGoals = goalsNow.length > 1 ? goalsNow.slice(1) : [];
1510
- const goal0 = applySubstTriple(rawGoal, substMut);
1511
-
1512
- // Builtins
1513
- const __pv0 = goal0.p instanceof Iri ? goal0.p.value : null;
1514
- const __rdfFirstOrRest = __pv0 === RDF_NS + 'first' || __pv0 === RDF_NS + 'rest';
1515
- const __treatBuiltin =
1516
- isBuiltinPred(goal0.p) &&
1517
- !(__rdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
1518
-
1519
- if (__treatBuiltin) {
1520
- const deltas = evalBuiltin(goal0, {}, facts, backRules, curDepth, varGen, 1);
1521
- for (const delta of deltas) {
1522
- const mark = trail.length;
1523
- if (!applyDeltaToSubst(delta)) {
1524
- undoTo(mark);
1525
- continue;
1526
- }
1527
- const ok = __existsGoals(restGoals, curDepth + 1, visitedNow);
1528
- undoTo(mark);
1529
- if (ok) return true;
1530
- }
1531
- return false;
1532
- }
1533
-
1534
- // Loop check for backward reasoning
1535
- if (listHasTriple(visitedNow, goal0)) return false;
1536
- const visitedForRules = visitedNow.concat([goal0]);
1537
-
1538
- // Ground-goal memo check (only safe when the goal is fully ground)
1539
- const memoKey = __memoKeyTriple(goal0);
1540
- if (memoKey) {
1541
- const mv = __groundGoalMemo.get(memoKey);
1542
- if (mv === __MEMO_FALSE) return false;
1543
- if (mv === __MEMO_TRUE_FACT) return __existsGoals(restGoals, curDepth + 1, visitedNow);
1544
- if (mv === __MEMO_TRUE_RULE) return __existsGoals(restGoals, curDepth + 1, visitedForRules);
1545
- if (mv === __MEMO_INPROGRESS) return false;
1546
-
1547
- __memoSet(memoKey, __MEMO_INPROGRESS);
1548
- const kind = __groundGoalSatisfiableKind(goal0, curDepth, visitedNow, visitedForRules);
1549
- __memoSet(memoKey, kind);
1550
-
1551
- if (kind === __MEMO_FALSE) return false;
1552
- if (kind === __MEMO_TRUE_FACT) return __existsGoals(restGoals, curDepth + 1, visitedNow);
1553
- return __existsGoals(restGoals, curDepth + 1, visitedForRules);
1554
- }
1555
-
1556
- // Backward rules (indexed by head predicate) — explored first
1557
- if (goal0.p instanceof Iri) {
1558
- ensureBackRuleIndexes(backRules);
1559
- const candRules = (backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
1560
-
1561
- for (const r of candRules) {
1562
- if (r.conclusion.length !== 1) continue;
1563
- const rawHead = r.conclusion[0];
1564
- if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
1565
-
1566
- const rStd = standardizeRule(r, varGen);
1567
- const head = rStd.conclusion[0];
1568
-
1569
- const mark = trail.length;
1570
- if (!unifyTripleTrail(head, goal0)) {
1571
- undoTo(mark);
1572
- continue;
1573
- }
1574
-
1575
- const newGoals = rStd.premise.concat(restGoals);
1576
- const ok = __existsGoals(newGoals, curDepth + 1, visitedForRules);
1577
- undoTo(mark);
1578
- if (ok) return true;
1579
- }
1580
- }
1581
-
1582
- // Facts
1583
- if (goal0.p instanceof Iri) {
1584
- const candidates = candidateFacts(facts, goal0);
1585
- for (const f of candidates) {
1586
- const mark = trail.length;
1587
- if (!unifyTripleTrail(goal0, f)) {
1588
- undoTo(mark);
1589
- continue;
1590
- }
1591
- const ok = __existsGoals(restGoals, curDepth + 1, visitedNow);
1592
- undoTo(mark);
1593
- if (ok) return true;
1594
- }
1595
- return false;
1596
- }
1597
-
1598
- for (const f of facts) {
1599
- const mark = trail.length;
1600
- if (!unifyTripleTrail(goal0, f)) {
1601
- undoTo(mark);
1602
- continue;
1603
- }
1604
- const ok = __existsGoals(restGoals, curDepth + 1, visitedNow);
1605
- undoTo(mark);
1606
- if (ok) return true;
1607
- }
1608
- return false;
1609
- }
1610
-
1611
- function __groundGoalSatisfiableKind(goal0, curDepth, visitedNow, visitedForRules) {
1612
- // Try direct facts first (cheap): if any fact matches, we can treat this as "fact satisfiable"
1613
- // for visited semantics.
1614
- if (goal0.p instanceof Iri) {
1615
- const candidates = candidateFacts(facts, goal0);
1616
- for (const f of candidates) {
1617
- const mark = trail.length;
1618
- const ok = unifyTripleTrail(goal0, f);
1619
- undoTo(mark);
1620
- if (ok) return __MEMO_TRUE_FACT;
1621
- }
1622
- } else {
1623
- for (const f of facts) {
1624
- const mark = trail.length;
1625
- const ok = unifyTripleTrail(goal0, f);
1626
- undoTo(mark);
1627
- if (ok) return __MEMO_TRUE_FACT;
1628
- }
1629
- }
1630
-
1631
- // Otherwise, look for a proof via backward rules.
1632
- if (goal0.p instanceof Iri) {
1633
- ensureBackRuleIndexes(backRules);
1634
- const candRules = (backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
1635
-
1636
- for (const r of candRules) {
1637
- if (r.conclusion.length !== 1) continue;
1638
- const rawHead = r.conclusion[0];
1639
- if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
1640
-
1641
- const rStd = standardizeRule(r, varGen);
1642
- const head = rStd.conclusion[0];
1643
-
1644
- const mark = trail.length;
1645
- if (!unifyTripleTrail(head, goal0)) {
1646
- undoTo(mark);
1647
- continue;
1648
- }
1649
-
1650
- const ok = __existsGoals(rStd.premise, curDepth + 1, visitedForRules);
1651
- undoTo(mark);
1652
- if (ok) return __MEMO_TRUE_RULE;
1653
- }
1654
- }
1655
-
1656
- return __MEMO_FALSE;
1657
- }
1658
-
1659
1452
  function dfs(goalsNow, curDepth, visitedNow, canDeferBuiltins, deferCount) {
1660
1453
  if (results.length >= max) return;
1661
1454
  if (!goalsNow.length) {
@@ -1732,50 +1525,15 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
1732
1525
  if (listHasTriple(visitedNow, goal0)) return;
1733
1526
  const visitedForRules = visitedNow.concat([goal0]);
1734
1527
 
1735
- // Ground-goal memoization: if a goal is fully ground, it cannot introduce new bindings,
1736
- // so we can cache whether it is satisfiable and skip re-proving it across branches.
1737
- const __memoKey0 = __memoKeyTriple(goal0);
1738
- if (__memoKey0) {
1739
- const mv = __groundGoalMemo.get(__memoKey0);
1740
- if (mv === __MEMO_FALSE) return;
1741
-
1742
- if (mv === __MEMO_TRUE_FACT || mv === __MEMO_TRUE_RULE) {
1743
- const __nextVisited = mv === __MEMO_TRUE_FACT ? visitedNow : visitedForRules;
1744
- if (!restGoals.length) {
1745
- results.push(gcCompactForGoals(substMut, [], answerVars));
1746
- } else {
1747
- dfs(restGoals, curDepth + 1, __nextVisited, canDeferBuiltins, 0);
1748
- }
1749
- return;
1750
- }
1751
-
1752
- // Cycle breaker for recursive ground goals.
1753
- if (mv === __MEMO_INPROGRESS) return;
1754
-
1755
- __memoSet(__memoKey0, __MEMO_INPROGRESS);
1756
- const kind = __groundGoalSatisfiableKind(goal0, curDepth, visitedNow, visitedForRules);
1757
- __memoSet(__memoKey0, kind);
1758
-
1759
- if (kind === __MEMO_FALSE) return;
1760
-
1761
- const __nextVisited = kind === __MEMO_TRUE_FACT ? visitedNow : visitedForRules;
1762
- if (!restGoals.length) {
1763
- results.push(gcCompactForGoals(substMut, [], answerVars));
1764
- } else {
1765
- dfs(restGoals, curDepth + 1, __nextVisited, canDeferBuiltins, 0);
1766
- }
1767
- return;
1768
- }
1769
-
1770
1528
  // 3) Backward rules (indexed by head predicate) — explored first
1771
1529
  if (goal0.p instanceof Iri) {
1772
1530
  ensureBackRuleIndexes(backRules);
1773
- const candRules = (backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
1531
+ const candRules = (backRules.__byHeadPred.get(goal0.p.__tid) || []).concat(backRules.__wildHeadPred);
1774
1532
 
1775
1533
  for (const r of candRules) {
1776
1534
  if (r.conclusion.length !== 1) continue;
1777
1535
  const rawHead = r.conclusion[0];
1778
- if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
1536
+ if (rawHead.p instanceof Iri && rawHead.p.__tid !== goal0.p.__tid) continue;
1779
1537
 
1780
1538
  const rStd = standardizeRule(r, varGen);
1781
1539
  const head = rStd.conclusion[0];
@@ -1801,7 +1559,8 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
1801
1559
  // 4) Try to satisfy the goal from known facts
1802
1560
  if (goal0.p instanceof Iri) {
1803
1561
  const candidates = candidateFacts(facts, goal0);
1804
- for (const f of candidates) {
1562
+ for (const idx of candidates) {
1563
+ const f = facts[idx];
1805
1564
  const mark = trail.length;
1806
1565
  if (!unifyTripleTrail(goal0, f)) {
1807
1566
  undoTo(mark);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.11.19",
3
+ "version": "1.11.21",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [