eyeling 1.11.9 → 1.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HANDBOOK.md +19 -3
- package/eyeling.js +126 -57
- package/lib/engine.js +126 -57
- package/package.json +1 -1
package/HANDBOOK.md
CHANGED
|
@@ -365,11 +365,23 @@ A substitution is a plain JS object:
|
|
|
365
365
|
{ X: Term, Y: Term, ... }
|
|
366
366
|
```
|
|
367
367
|
|
|
368
|
-
When applying substitutions, Eyeling follows chains
|
|
368
|
+
When applying substitutions, Eyeling follows **chains**:
|
|
369
369
|
|
|
370
370
|
* if `X → Var(Y)` and `Y → Iri(...)`, applying to `X` yields the IRI.
|
|
371
371
|
|
|
372
|
-
|
|
372
|
+
Chains arise naturally during unification (e.g. when variables unify with other variables) and during rule firing. Eyeling treats substitutions as *persistent maps*: when a new binding is added, the engine makes a shallow copy and records only the new binding.
|
|
373
|
+
|
|
374
|
+
Implementation details (and why they matter):
|
|
375
|
+
|
|
376
|
+
* **`applySubstTerm` is the only “chain chaser”.** It follows `Var → Term` links until it reaches a stable term.
|
|
377
|
+
* Unification’s occurs-check prevents most cycles, but `applySubstTerm` still defends against accidental cyclic chains.
|
|
378
|
+
* The cycle guard is written to avoid allocating a `Set` in the common case (short chains).
|
|
379
|
+
* **Structural sharing is deliberate.** Applying a substitution often changes nothing:
|
|
380
|
+
* `applySubstTerm` returns the original term when it is unaffected.
|
|
381
|
+
* list/open-list/graph terms are only rebuilt if at least one component changes (lazy copy-on-change).
|
|
382
|
+
* `applySubstTriple` returns the original `Triple` when `s/p/o` are unchanged.
|
|
383
|
+
|
|
384
|
+
These “no-op returns” are one of the biggest practical performance wins in the engine: backward chaining and forward rule instantiation apply substitutions constantly, so avoiding allocations reduces GC pressure without changing semantics.
|
|
373
385
|
|
|
374
386
|
### 6.4 Unification: the core operation
|
|
375
387
|
|
|
@@ -480,6 +492,9 @@ for delta in deltas:
|
|
|
480
492
|
composed = composeSubst(currentSubst, delta)
|
|
481
493
|
```
|
|
482
494
|
|
|
495
|
+
**Implementation note (performance):** `composeSubst` has a fast path — if `delta` is empty (or only repeats bindings already present in `currentSubst`), it returns the original substitution object instead of cloning. This makes constraint-style builtins that often yield `{}` much cheaper, but it does **not** change the search-space: a vacuous solution can still amplify later branching.
|
|
496
|
+
|
|
497
|
+
|
|
483
498
|
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.
|
|
484
499
|
|
|
485
500
|
#### 8.3.1 Builtin deferral and “vacuous” solutions
|
|
@@ -579,6 +594,8 @@ But it does something subtle and important: it caches skolemization per (rule fi
|
|
|
579
594
|
|
|
580
595
|
The “firing instance” is keyed by a deterministic string derived from the instantiated body (“firingKey”). This stabilizes the closure and prevents “existential churn.”
|
|
581
596
|
|
|
597
|
+
**Implementation note (performance):** the firing-instance key is computed in a hot loop, so `firingKey(...)` builds a compact string via concatenation rather than `JSON.stringify`. If you change what counts as a distinct “firing instance”, update the key format and the skolem cache together.
|
|
598
|
+
|
|
582
599
|
Implementation: deterministic Skolem IDs live in `lib/skolem.js`; the per-firing cache and head-blank rewriting are implemented in `lib/engine.js`.
|
|
583
600
|
|
|
584
601
|
### 9.4 Inference fuses: `{ ... } => false`
|
|
@@ -1878,4 +1895,3 @@ Logic & reasoning background (Wikipedia):
|
|
|
1878
1895
|
- [https://en.wikipedia.org/wiki/Prolog](https://en.wikipedia.org/wiki/Prolog)
|
|
1879
1896
|
- [https://en.wikipedia.org/wiki/Datalog](https://en.wikipedia.org/wiki/Datalog)
|
|
1880
1897
|
- [https://en.wikipedia.org/wiki/Skolem_normal_form](https://en.wikipedia.org/wiki/Skolem_normal_form)
|
|
1881
|
-
|
package/eyeling.js
CHANGED
|
@@ -5226,64 +5226,123 @@ function skolemIriFromGroundTerm(t) {
|
|
|
5226
5226
|
function applySubstTerm(t, s) {
|
|
5227
5227
|
// Common case: variable
|
|
5228
5228
|
if (t instanceof Var) {
|
|
5229
|
-
// Fast path: unbound variable → no change
|
|
5230
5229
|
const first = s[t.name];
|
|
5231
|
-
if (first === undefined)
|
|
5232
|
-
return t;
|
|
5233
|
-
}
|
|
5230
|
+
if (first === undefined) return t;
|
|
5234
5231
|
|
|
5235
5232
|
// Follow chains X -> Y -> ... until we hit a non-var or a cycle.
|
|
5233
|
+
// Avoid allocating a Set in the common case (short chains).
|
|
5236
5234
|
let cur = first;
|
|
5237
|
-
|
|
5235
|
+
let seen0 = t.name;
|
|
5236
|
+
let seen1 = null;
|
|
5237
|
+
let seen2 = null;
|
|
5238
|
+
let seenSet = null;
|
|
5239
|
+
let steps = 0;
|
|
5240
|
+
|
|
5238
5241
|
while (cur instanceof Var) {
|
|
5239
5242
|
const name = cur.name;
|
|
5240
|
-
|
|
5241
|
-
|
|
5243
|
+
|
|
5244
|
+
// Cycle check
|
|
5245
|
+
if (name === seen0 || name === seen1 || name === seen2 || (seenSet && seenSet.has(name))) {
|
|
5246
|
+
return cur;
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5249
|
+
if (steps == 0) {
|
|
5250
|
+
seen1 = name;
|
|
5251
|
+
} else if (steps == 1) {
|
|
5252
|
+
seen2 = name;
|
|
5253
|
+
} else if (steps == 2) {
|
|
5254
|
+
seenSet = new Set([seen0, seen1, seen2]);
|
|
5255
|
+
seenSet.add(name);
|
|
5256
|
+
} else if (seenSet) {
|
|
5257
|
+
seenSet.add(name);
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5242
5260
|
const nxt = s[name];
|
|
5243
|
-
if (
|
|
5261
|
+
if (nxt === undefined) break;
|
|
5244
5262
|
cur = nxt;
|
|
5245
|
-
}
|
|
5246
5263
|
|
|
5247
|
-
|
|
5248
|
-
//
|
|
5249
|
-
|
|
5264
|
+
steps += 1;
|
|
5265
|
+
// Safety guard against pathological substitutions
|
|
5266
|
+
if (steps > 1024) break;
|
|
5250
5267
|
}
|
|
5251
|
-
|
|
5252
|
-
|
|
5268
|
+
|
|
5269
|
+
if (cur instanceof Var) return cur;
|
|
5270
|
+
// Bound to a non-var term: apply substitution recursively in case it contains variables inside.
|
|
5253
5271
|
return applySubstTerm(cur, s);
|
|
5254
5272
|
}
|
|
5255
5273
|
|
|
5256
5274
|
// Non-variable terms
|
|
5257
5275
|
if (t instanceof ListTerm) {
|
|
5258
|
-
|
|
5276
|
+
const xs = t.elems;
|
|
5277
|
+
let out = null;
|
|
5278
|
+
for (let i = 0; i < xs.length; i++) {
|
|
5279
|
+
const v = applySubstTerm(xs[i], s);
|
|
5280
|
+
if (out) {
|
|
5281
|
+
out.push(v);
|
|
5282
|
+
} else if (v !== xs[i]) {
|
|
5283
|
+
out = xs.slice(0, i);
|
|
5284
|
+
out.push(v);
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
return out ? new ListTerm(out) : t;
|
|
5259
5288
|
}
|
|
5260
5289
|
|
|
5261
5290
|
if (t instanceof OpenListTerm) {
|
|
5262
|
-
const
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
const
|
|
5266
|
-
if (
|
|
5267
|
-
|
|
5268
|
-
} else if (
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
return new OpenListTerm(newPrefix, t.tailVar);
|
|
5291
|
+
const xs = t.prefix;
|
|
5292
|
+
let newPrefix = null;
|
|
5293
|
+
for (let i = 0; i < xs.length; i++) {
|
|
5294
|
+
const v = applySubstTerm(xs[i], s);
|
|
5295
|
+
if (newPrefix) {
|
|
5296
|
+
newPrefix.push(v);
|
|
5297
|
+
} else if (v !== xs[i]) {
|
|
5298
|
+
newPrefix = xs.slice(0, i);
|
|
5299
|
+
newPrefix.push(v);
|
|
5272
5300
|
}
|
|
5301
|
+
}
|
|
5302
|
+
const prefixApplied = newPrefix || xs;
|
|
5303
|
+
|
|
5304
|
+
const tailTerm = s[t.tailVar];
|
|
5305
|
+
if (tailTerm === undefined) {
|
|
5306
|
+
return prefixApplied === xs ? t : new OpenListTerm(prefixApplied, t.tailVar);
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
const tailApplied = applySubstTerm(tailTerm, s);
|
|
5310
|
+
if (tailApplied instanceof ListTerm) {
|
|
5311
|
+
if (prefixApplied.length === 0) return tailApplied;
|
|
5312
|
+
return new ListTerm(prefixApplied.concat(tailApplied.elems));
|
|
5313
|
+
} else if (tailApplied instanceof OpenListTerm) {
|
|
5314
|
+
if (prefixApplied.length === 0) return tailApplied;
|
|
5315
|
+
return new OpenListTerm(prefixApplied.concat(tailApplied.prefix), tailApplied.tailVar);
|
|
5273
5316
|
} else {
|
|
5274
|
-
|
|
5317
|
+
// Non-list tail binding: keep as open list (matches existing behavior).
|
|
5318
|
+
return prefixApplied === xs ? t : new OpenListTerm(prefixApplied, t.tailVar);
|
|
5275
5319
|
}
|
|
5276
5320
|
}
|
|
5277
5321
|
|
|
5278
5322
|
if (t instanceof GraphTerm) {
|
|
5279
|
-
|
|
5323
|
+
const xs = t.triples;
|
|
5324
|
+
let out = null;
|
|
5325
|
+
for (let i = 0; i < xs.length; i++) {
|
|
5326
|
+
const v = applySubstTriple(xs[i], s);
|
|
5327
|
+
if (out) {
|
|
5328
|
+
out.push(v);
|
|
5329
|
+
} else if (v !== xs[i]) {
|
|
5330
|
+
out = xs.slice(0, i);
|
|
5331
|
+
out.push(v);
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
return out ? new GraphTerm(out) : t;
|
|
5280
5335
|
}
|
|
5281
5336
|
|
|
5282
5337
|
return t;
|
|
5283
5338
|
}
|
|
5284
5339
|
|
|
5285
5340
|
function applySubstTriple(tr, s) {
|
|
5286
|
-
|
|
5341
|
+
const s2 = applySubstTerm(tr.s, s);
|
|
5342
|
+
const p2 = applySubstTerm(tr.p, s);
|
|
5343
|
+
const o2 = applySubstTerm(tr.o, s);
|
|
5344
|
+
if (s2 === tr.s && p2 === tr.p && o2 === tr.o) return tr;
|
|
5345
|
+
return new Triple(s2, p2, o2);
|
|
5287
5346
|
}
|
|
5288
5347
|
|
|
5289
5348
|
function iriValue(t) {
|
|
@@ -5292,7 +5351,7 @@ function iriValue(t) {
|
|
|
5292
5351
|
|
|
5293
5352
|
function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
5294
5353
|
if (ys.length < prefix.length) return null;
|
|
5295
|
-
let s2 =
|
|
5354
|
+
let s2 = subst;
|
|
5296
5355
|
for (let i = 0; i < prefix.length; i++) {
|
|
5297
5356
|
s2 = unifyTerm(prefix[i], ys[i], s2);
|
|
5298
5357
|
if (s2 === null) return null;
|
|
@@ -5307,7 +5366,7 @@ function unifyGraphTriples(xs, ys, subst) {
|
|
|
5307
5366
|
if (xs.length !== ys.length) return null;
|
|
5308
5367
|
|
|
5309
5368
|
// Fast path: exact same sequence.
|
|
5310
|
-
if (triplesListEqual(xs, ys)) return
|
|
5369
|
+
if (triplesListEqual(xs, ys)) return subst;
|
|
5311
5370
|
|
|
5312
5371
|
// Backtracking match (order-insensitive), *threading* the substitution through.
|
|
5313
5372
|
const used = new Array(ys.length).fill(false);
|
|
@@ -5334,7 +5393,7 @@ function unifyGraphTriples(xs, ys, subst) {
|
|
|
5334
5393
|
return null;
|
|
5335
5394
|
}
|
|
5336
5395
|
|
|
5337
|
-
return step(0,
|
|
5396
|
+
return step(0, subst); // IMPORTANT: start from the incoming subst
|
|
5338
5397
|
}
|
|
5339
5398
|
|
|
5340
5399
|
function unifyTerm(a, b, subst) {
|
|
@@ -5361,7 +5420,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5361
5420
|
if (a instanceof Var) {
|
|
5362
5421
|
const v = a.name;
|
|
5363
5422
|
const t = b;
|
|
5364
|
-
if (t instanceof Var && t.name === v) return
|
|
5423
|
+
if (t instanceof Var && t.name === v) return subst;
|
|
5365
5424
|
if (containsVarTerm(t, v)) return null;
|
|
5366
5425
|
const s2 = { ...subst };
|
|
5367
5426
|
s2[v] = t;
|
|
@@ -5372,20 +5431,20 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5372
5431
|
}
|
|
5373
5432
|
|
|
5374
5433
|
// Exact matches
|
|
5375
|
-
if (a instanceof Iri && b instanceof Iri && a.value === b.value) return
|
|
5376
|
-
if (a instanceof Literal && b instanceof Literal && a.value === b.value) return
|
|
5377
|
-
if (a instanceof Blank && b instanceof Blank && a.label === b.label) return
|
|
5434
|
+
if (a instanceof Iri && b instanceof Iri && a.value === b.value) return subst;
|
|
5435
|
+
if (a instanceof Literal && b instanceof Literal && a.value === b.value) return subst;
|
|
5436
|
+
if (a instanceof Blank && b instanceof Blank && a.label === b.label) return subst;
|
|
5378
5437
|
|
|
5379
5438
|
// Plain string vs xsd:string equivalence
|
|
5380
5439
|
if (a instanceof Literal && b instanceof Literal) {
|
|
5381
|
-
if (literalsEquivalentAsXsdString(a.value, b.value)) return
|
|
5440
|
+
if (literalsEquivalentAsXsdString(a.value, b.value)) return subst;
|
|
5382
5441
|
}
|
|
5383
5442
|
|
|
5384
5443
|
// Boolean-value equivalence (ONLY for normal unifyTerm)
|
|
5385
5444
|
if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
|
|
5386
5445
|
const ai = parseBooleanLiteralInfo(a);
|
|
5387
5446
|
const bi = parseBooleanLiteralInfo(b);
|
|
5388
|
-
if (ai && bi && ai.value === bi.value) return
|
|
5447
|
+
if (ai && bi && ai.value === bi.value) return subst;
|
|
5389
5448
|
}
|
|
5390
5449
|
|
|
5391
5450
|
// Numeric-value match:
|
|
@@ -5397,11 +5456,11 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5397
5456
|
if (ai && bi) {
|
|
5398
5457
|
if (ai.dt === bi.dt) {
|
|
5399
5458
|
if (ai.kind === 'bigint' && bi.kind === 'bigint') {
|
|
5400
|
-
if (ai.value === bi.value) return
|
|
5459
|
+
if (ai.value === bi.value) return subst;
|
|
5401
5460
|
} else {
|
|
5402
5461
|
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
5403
5462
|
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
5404
|
-
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return
|
|
5463
|
+
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return subst;
|
|
5405
5464
|
}
|
|
5406
5465
|
}
|
|
5407
5466
|
|
|
@@ -5414,7 +5473,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5414
5473
|
const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
|
|
5415
5474
|
if (dec) {
|
|
5416
5475
|
const scaledInt = intInfo.value * pow10n(dec.scale);
|
|
5417
|
-
if (scaledInt === dec.num) return
|
|
5476
|
+
if (scaledInt === dec.num) return subst;
|
|
5418
5477
|
}
|
|
5419
5478
|
}
|
|
5420
5479
|
}
|
|
@@ -5432,7 +5491,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5432
5491
|
// Open list vs open list
|
|
5433
5492
|
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
5434
5493
|
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length) return null;
|
|
5435
|
-
let s2 =
|
|
5494
|
+
let s2 = subst;
|
|
5436
5495
|
for (let i = 0; i < a.prefix.length; i++) {
|
|
5437
5496
|
s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
|
|
5438
5497
|
if (s2 === null) return null;
|
|
@@ -5443,7 +5502,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5443
5502
|
// List terms
|
|
5444
5503
|
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
5445
5504
|
if (a.elems.length !== b.elems.length) return null;
|
|
5446
|
-
let s2 =
|
|
5505
|
+
let s2 = subst;
|
|
5447
5506
|
for (let i = 0; i < a.elems.length; i++) {
|
|
5448
5507
|
s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
|
|
5449
5508
|
if (s2 === null) return null;
|
|
@@ -5453,7 +5512,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
5453
5512
|
|
|
5454
5513
|
// Graphs
|
|
5455
5514
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
5456
|
-
if (alphaEqGraphTriples(a.triples, b.triples)) return
|
|
5515
|
+
if (alphaEqGraphTriples(a.triples, b.triples)) return subst;
|
|
5457
5516
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
5458
5517
|
}
|
|
5459
5518
|
|
|
@@ -5473,18 +5532,25 @@ function unifyTriple(pat, fact, subst) {
|
|
|
5473
5532
|
}
|
|
5474
5533
|
|
|
5475
5534
|
function composeSubst(outer, delta) {
|
|
5476
|
-
if (!delta
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5535
|
+
if (!delta) return outer;
|
|
5536
|
+
|
|
5537
|
+
// Fast path: avoid copying `outer` when `delta` is empty or only repeats existing bindings.
|
|
5538
|
+
let out = null;
|
|
5539
|
+
|
|
5540
|
+
for (const k in delta) {
|
|
5541
|
+
if (!Object.prototype.hasOwnProperty.call(delta, k)) continue;
|
|
5542
|
+
const v = delta[k];
|
|
5543
|
+
|
|
5544
|
+
if (Object.prototype.hasOwnProperty.call(outer, k)) {
|
|
5545
|
+
if (!termsEqual(outer[k], v)) return null;
|
|
5546
|
+
continue;
|
|
5485
5547
|
}
|
|
5548
|
+
|
|
5549
|
+
if (!out) out = { ...outer };
|
|
5550
|
+
out[k] = v;
|
|
5486
5551
|
}
|
|
5487
|
-
|
|
5552
|
+
|
|
5553
|
+
return out || outer;
|
|
5488
5554
|
}
|
|
5489
5555
|
|
|
5490
5556
|
|
|
@@ -5863,11 +5929,14 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
5863
5929
|
|
|
5864
5930
|
function firingKey(ruleIndex, instantiatedPremises) {
|
|
5865
5931
|
// Deterministic key derived from the instantiated body (ground per substitution).
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5932
|
+
// Avoid repeated JSON.stringify of arrays-of-strings (hot path).
|
|
5933
|
+
let out = `R${ruleIndex}|`;
|
|
5934
|
+
for (let i = 0; i < instantiatedPremises.length; i++) {
|
|
5935
|
+
const tr = instantiatedPremises[i];
|
|
5936
|
+
if (i) out += '\n';
|
|
5937
|
+
out += skolemKeyFromTerm(tr.s) + ' ' + skolemKeyFromTerm(tr.p) + ' ' + skolemKeyFromTerm(tr.o);
|
|
5869
5938
|
}
|
|
5870
|
-
return
|
|
5939
|
+
return out;
|
|
5871
5940
|
}
|
|
5872
5941
|
|
|
5873
5942
|
// Make rules visible to introspection builtins
|
package/lib/engine.js
CHANGED
|
@@ -859,64 +859,123 @@ function skolemIriFromGroundTerm(t) {
|
|
|
859
859
|
function applySubstTerm(t, s) {
|
|
860
860
|
// Common case: variable
|
|
861
861
|
if (t instanceof Var) {
|
|
862
|
-
// Fast path: unbound variable → no change
|
|
863
862
|
const first = s[t.name];
|
|
864
|
-
if (first === undefined)
|
|
865
|
-
return t;
|
|
866
|
-
}
|
|
863
|
+
if (first === undefined) return t;
|
|
867
864
|
|
|
868
865
|
// Follow chains X -> Y -> ... until we hit a non-var or a cycle.
|
|
866
|
+
// Avoid allocating a Set in the common case (short chains).
|
|
869
867
|
let cur = first;
|
|
870
|
-
|
|
868
|
+
let seen0 = t.name;
|
|
869
|
+
let seen1 = null;
|
|
870
|
+
let seen2 = null;
|
|
871
|
+
let seenSet = null;
|
|
872
|
+
let steps = 0;
|
|
873
|
+
|
|
871
874
|
while (cur instanceof Var) {
|
|
872
875
|
const name = cur.name;
|
|
873
|
-
|
|
874
|
-
|
|
876
|
+
|
|
877
|
+
// Cycle check
|
|
878
|
+
if (name === seen0 || name === seen1 || name === seen2 || (seenSet && seenSet.has(name))) {
|
|
879
|
+
return cur;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (steps == 0) {
|
|
883
|
+
seen1 = name;
|
|
884
|
+
} else if (steps == 1) {
|
|
885
|
+
seen2 = name;
|
|
886
|
+
} else if (steps == 2) {
|
|
887
|
+
seenSet = new Set([seen0, seen1, seen2]);
|
|
888
|
+
seenSet.add(name);
|
|
889
|
+
} else if (seenSet) {
|
|
890
|
+
seenSet.add(name);
|
|
891
|
+
}
|
|
892
|
+
|
|
875
893
|
const nxt = s[name];
|
|
876
|
-
if (
|
|
894
|
+
if (nxt === undefined) break;
|
|
877
895
|
cur = nxt;
|
|
878
|
-
}
|
|
879
896
|
|
|
880
|
-
|
|
881
|
-
//
|
|
882
|
-
|
|
897
|
+
steps += 1;
|
|
898
|
+
// Safety guard against pathological substitutions
|
|
899
|
+
if (steps > 1024) break;
|
|
883
900
|
}
|
|
884
|
-
|
|
885
|
-
|
|
901
|
+
|
|
902
|
+
if (cur instanceof Var) return cur;
|
|
903
|
+
// Bound to a non-var term: apply substitution recursively in case it contains variables inside.
|
|
886
904
|
return applySubstTerm(cur, s);
|
|
887
905
|
}
|
|
888
906
|
|
|
889
907
|
// Non-variable terms
|
|
890
908
|
if (t instanceof ListTerm) {
|
|
891
|
-
|
|
909
|
+
const xs = t.elems;
|
|
910
|
+
let out = null;
|
|
911
|
+
for (let i = 0; i < xs.length; i++) {
|
|
912
|
+
const v = applySubstTerm(xs[i], s);
|
|
913
|
+
if (out) {
|
|
914
|
+
out.push(v);
|
|
915
|
+
} else if (v !== xs[i]) {
|
|
916
|
+
out = xs.slice(0, i);
|
|
917
|
+
out.push(v);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return out ? new ListTerm(out) : t;
|
|
892
921
|
}
|
|
893
922
|
|
|
894
923
|
if (t instanceof OpenListTerm) {
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
const
|
|
899
|
-
if (
|
|
900
|
-
|
|
901
|
-
} else if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return new OpenListTerm(newPrefix, t.tailVar);
|
|
924
|
+
const xs = t.prefix;
|
|
925
|
+
let newPrefix = null;
|
|
926
|
+
for (let i = 0; i < xs.length; i++) {
|
|
927
|
+
const v = applySubstTerm(xs[i], s);
|
|
928
|
+
if (newPrefix) {
|
|
929
|
+
newPrefix.push(v);
|
|
930
|
+
} else if (v !== xs[i]) {
|
|
931
|
+
newPrefix = xs.slice(0, i);
|
|
932
|
+
newPrefix.push(v);
|
|
905
933
|
}
|
|
934
|
+
}
|
|
935
|
+
const prefixApplied = newPrefix || xs;
|
|
936
|
+
|
|
937
|
+
const tailTerm = s[t.tailVar];
|
|
938
|
+
if (tailTerm === undefined) {
|
|
939
|
+
return prefixApplied === xs ? t : new OpenListTerm(prefixApplied, t.tailVar);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const tailApplied = applySubstTerm(tailTerm, s);
|
|
943
|
+
if (tailApplied instanceof ListTerm) {
|
|
944
|
+
if (prefixApplied.length === 0) return tailApplied;
|
|
945
|
+
return new ListTerm(prefixApplied.concat(tailApplied.elems));
|
|
946
|
+
} else if (tailApplied instanceof OpenListTerm) {
|
|
947
|
+
if (prefixApplied.length === 0) return tailApplied;
|
|
948
|
+
return new OpenListTerm(prefixApplied.concat(tailApplied.prefix), tailApplied.tailVar);
|
|
906
949
|
} else {
|
|
907
|
-
|
|
950
|
+
// Non-list tail binding: keep as open list (matches existing behavior).
|
|
951
|
+
return prefixApplied === xs ? t : new OpenListTerm(prefixApplied, t.tailVar);
|
|
908
952
|
}
|
|
909
953
|
}
|
|
910
954
|
|
|
911
955
|
if (t instanceof GraphTerm) {
|
|
912
|
-
|
|
956
|
+
const xs = t.triples;
|
|
957
|
+
let out = null;
|
|
958
|
+
for (let i = 0; i < xs.length; i++) {
|
|
959
|
+
const v = applySubstTriple(xs[i], s);
|
|
960
|
+
if (out) {
|
|
961
|
+
out.push(v);
|
|
962
|
+
} else if (v !== xs[i]) {
|
|
963
|
+
out = xs.slice(0, i);
|
|
964
|
+
out.push(v);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return out ? new GraphTerm(out) : t;
|
|
913
968
|
}
|
|
914
969
|
|
|
915
970
|
return t;
|
|
916
971
|
}
|
|
917
972
|
|
|
918
973
|
function applySubstTriple(tr, s) {
|
|
919
|
-
|
|
974
|
+
const s2 = applySubstTerm(tr.s, s);
|
|
975
|
+
const p2 = applySubstTerm(tr.p, s);
|
|
976
|
+
const o2 = applySubstTerm(tr.o, s);
|
|
977
|
+
if (s2 === tr.s && p2 === tr.p && o2 === tr.o) return tr;
|
|
978
|
+
return new Triple(s2, p2, o2);
|
|
920
979
|
}
|
|
921
980
|
|
|
922
981
|
function iriValue(t) {
|
|
@@ -925,7 +984,7 @@ function iriValue(t) {
|
|
|
925
984
|
|
|
926
985
|
function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
927
986
|
if (ys.length < prefix.length) return null;
|
|
928
|
-
let s2 =
|
|
987
|
+
let s2 = subst;
|
|
929
988
|
for (let i = 0; i < prefix.length; i++) {
|
|
930
989
|
s2 = unifyTerm(prefix[i], ys[i], s2);
|
|
931
990
|
if (s2 === null) return null;
|
|
@@ -940,7 +999,7 @@ function unifyGraphTriples(xs, ys, subst) {
|
|
|
940
999
|
if (xs.length !== ys.length) return null;
|
|
941
1000
|
|
|
942
1001
|
// Fast path: exact same sequence.
|
|
943
|
-
if (triplesListEqual(xs, ys)) return
|
|
1002
|
+
if (triplesListEqual(xs, ys)) return subst;
|
|
944
1003
|
|
|
945
1004
|
// Backtracking match (order-insensitive), *threading* the substitution through.
|
|
946
1005
|
const used = new Array(ys.length).fill(false);
|
|
@@ -967,7 +1026,7 @@ function unifyGraphTriples(xs, ys, subst) {
|
|
|
967
1026
|
return null;
|
|
968
1027
|
}
|
|
969
1028
|
|
|
970
|
-
return step(0,
|
|
1029
|
+
return step(0, subst); // IMPORTANT: start from the incoming subst
|
|
971
1030
|
}
|
|
972
1031
|
|
|
973
1032
|
function unifyTerm(a, b, subst) {
|
|
@@ -994,7 +1053,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
994
1053
|
if (a instanceof Var) {
|
|
995
1054
|
const v = a.name;
|
|
996
1055
|
const t = b;
|
|
997
|
-
if (t instanceof Var && t.name === v) return
|
|
1056
|
+
if (t instanceof Var && t.name === v) return subst;
|
|
998
1057
|
if (containsVarTerm(t, v)) return null;
|
|
999
1058
|
const s2 = { ...subst };
|
|
1000
1059
|
s2[v] = t;
|
|
@@ -1005,20 +1064,20 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1005
1064
|
}
|
|
1006
1065
|
|
|
1007
1066
|
// Exact matches
|
|
1008
|
-
if (a instanceof Iri && b instanceof Iri && a.value === b.value) return
|
|
1009
|
-
if (a instanceof Literal && b instanceof Literal && a.value === b.value) return
|
|
1010
|
-
if (a instanceof Blank && b instanceof Blank && a.label === b.label) return
|
|
1067
|
+
if (a instanceof Iri && b instanceof Iri && a.value === b.value) return subst;
|
|
1068
|
+
if (a instanceof Literal && b instanceof Literal && a.value === b.value) return subst;
|
|
1069
|
+
if (a instanceof Blank && b instanceof Blank && a.label === b.label) return subst;
|
|
1011
1070
|
|
|
1012
1071
|
// Plain string vs xsd:string equivalence
|
|
1013
1072
|
if (a instanceof Literal && b instanceof Literal) {
|
|
1014
|
-
if (literalsEquivalentAsXsdString(a.value, b.value)) return
|
|
1073
|
+
if (literalsEquivalentAsXsdString(a.value, b.value)) return subst;
|
|
1015
1074
|
}
|
|
1016
1075
|
|
|
1017
1076
|
// Boolean-value equivalence (ONLY for normal unifyTerm)
|
|
1018
1077
|
if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
|
|
1019
1078
|
const ai = parseBooleanLiteralInfo(a);
|
|
1020
1079
|
const bi = parseBooleanLiteralInfo(b);
|
|
1021
|
-
if (ai && bi && ai.value === bi.value) return
|
|
1080
|
+
if (ai && bi && ai.value === bi.value) return subst;
|
|
1022
1081
|
}
|
|
1023
1082
|
|
|
1024
1083
|
// Numeric-value match:
|
|
@@ -1030,11 +1089,11 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1030
1089
|
if (ai && bi) {
|
|
1031
1090
|
if (ai.dt === bi.dt) {
|
|
1032
1091
|
if (ai.kind === 'bigint' && bi.kind === 'bigint') {
|
|
1033
|
-
if (ai.value === bi.value) return
|
|
1092
|
+
if (ai.value === bi.value) return subst;
|
|
1034
1093
|
} else {
|
|
1035
1094
|
const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
|
|
1036
1095
|
const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
|
|
1037
|
-
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return
|
|
1096
|
+
if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return subst;
|
|
1038
1097
|
}
|
|
1039
1098
|
}
|
|
1040
1099
|
|
|
@@ -1047,7 +1106,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1047
1106
|
const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
|
|
1048
1107
|
if (dec) {
|
|
1049
1108
|
const scaledInt = intInfo.value * pow10n(dec.scale);
|
|
1050
|
-
if (scaledInt === dec.num) return
|
|
1109
|
+
if (scaledInt === dec.num) return subst;
|
|
1051
1110
|
}
|
|
1052
1111
|
}
|
|
1053
1112
|
}
|
|
@@ -1065,7 +1124,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1065
1124
|
// Open list vs open list
|
|
1066
1125
|
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
1067
1126
|
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length) return null;
|
|
1068
|
-
let s2 =
|
|
1127
|
+
let s2 = subst;
|
|
1069
1128
|
for (let i = 0; i < a.prefix.length; i++) {
|
|
1070
1129
|
s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
|
|
1071
1130
|
if (s2 === null) return null;
|
|
@@ -1076,7 +1135,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1076
1135
|
// List terms
|
|
1077
1136
|
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
1078
1137
|
if (a.elems.length !== b.elems.length) return null;
|
|
1079
|
-
let s2 =
|
|
1138
|
+
let s2 = subst;
|
|
1080
1139
|
for (let i = 0; i < a.elems.length; i++) {
|
|
1081
1140
|
s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
|
|
1082
1141
|
if (s2 === null) return null;
|
|
@@ -1086,7 +1145,7 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1086
1145
|
|
|
1087
1146
|
// Graphs
|
|
1088
1147
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
1089
|
-
if (alphaEqGraphTriples(a.triples, b.triples)) return
|
|
1148
|
+
if (alphaEqGraphTriples(a.triples, b.triples)) return subst;
|
|
1090
1149
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
1091
1150
|
}
|
|
1092
1151
|
|
|
@@ -1106,18 +1165,25 @@ function unifyTriple(pat, fact, subst) {
|
|
|
1106
1165
|
}
|
|
1107
1166
|
|
|
1108
1167
|
function composeSubst(outer, delta) {
|
|
1109
|
-
if (!delta
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1168
|
+
if (!delta) return outer;
|
|
1169
|
+
|
|
1170
|
+
// Fast path: avoid copying `outer` when `delta` is empty or only repeats existing bindings.
|
|
1171
|
+
let out = null;
|
|
1172
|
+
|
|
1173
|
+
for (const k in delta) {
|
|
1174
|
+
if (!Object.prototype.hasOwnProperty.call(delta, k)) continue;
|
|
1175
|
+
const v = delta[k];
|
|
1176
|
+
|
|
1177
|
+
if (Object.prototype.hasOwnProperty.call(outer, k)) {
|
|
1178
|
+
if (!termsEqual(outer[k], v)) return null;
|
|
1179
|
+
continue;
|
|
1118
1180
|
}
|
|
1181
|
+
|
|
1182
|
+
if (!out) out = { ...outer };
|
|
1183
|
+
out[k] = v;
|
|
1119
1184
|
}
|
|
1120
|
-
|
|
1185
|
+
|
|
1186
|
+
return out || outer;
|
|
1121
1187
|
}
|
|
1122
1188
|
|
|
1123
1189
|
|
|
@@ -1496,11 +1562,14 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
1496
1562
|
|
|
1497
1563
|
function firingKey(ruleIndex, instantiatedPremises) {
|
|
1498
1564
|
// Deterministic key derived from the instantiated body (ground per substitution).
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1565
|
+
// Avoid repeated JSON.stringify of arrays-of-strings (hot path).
|
|
1566
|
+
let out = `R${ruleIndex}|`;
|
|
1567
|
+
for (let i = 0; i < instantiatedPremises.length; i++) {
|
|
1568
|
+
const tr = instantiatedPremises[i];
|
|
1569
|
+
if (i) out += '\n';
|
|
1570
|
+
out += skolemKeyFromTerm(tr.s) + ' ' + skolemKeyFromTerm(tr.p) + ' ' + skolemKeyFromTerm(tr.o);
|
|
1502
1571
|
}
|
|
1503
|
-
return
|
|
1572
|
+
return out;
|
|
1504
1573
|
}
|
|
1505
1574
|
|
|
1506
1575
|
// Make rules visible to introspection builtins
|