eyeling 1.15.9 → 1.15.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 +4 -0
- package/README.md +2 -0
- package/examples/french-cities.n3 +2 -0
- package/examples/jsonterm-advanced.n3 +92 -0
- package/examples/jsonterm.n3 +37 -0
- package/examples/output/french-cities.n3 +1 -18
- package/examples/output/jsonterm-advanced.n3 +8 -0
- package/examples/output/jsonterm.n3 +3 -0
- package/eyeling.js +99 -25
- package/lib/engine.js +99 -25
- package/package.json +1 -1
- package/test/api.test.js +28 -0
package/HANDBOOK.md
CHANGED
|
@@ -376,6 +376,10 @@ That’s alpha-equivalence:
|
|
|
376
376
|
|
|
377
377
|
Eyeling implements alpha-equivalence by checking whether there exists a consistent renaming mapping between the two formulas’ variables/blanks that makes the triples match.
|
|
378
378
|
|
|
379
|
+
Important scope nuance: only blanks/variables that are local to the quoted formula participate in alpha-renaming. If a formula is being matched after an outer substitution has already instantiated part of it, those substituted terms are treated as fixed. In other words, alpha-equivalence may rename formula-local placeholders, but it must not rename names that came from the enclosing match. This prevents a substituted outer blank node from being confused with a local blank node inside the quoted formula.
|
|
380
|
+
|
|
381
|
+
So `{ _:x :p :o }` obtained by substituting `?A = _:x` into `{ ?A :p :o }` must not alpha-match `{ _:b :p :o }` by renaming `_:x` to `_:b`.
|
|
382
|
+
|
|
379
383
|
### 6.2 Groundness: “variables inside formulas don’t leak”
|
|
380
384
|
|
|
381
385
|
Eyeling makes a deliberate choice about _groundness_:
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# eyeling
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/eyereasoner) [](https://doi.org/10.5281/zenodo.19068086)
|
|
4
|
+
|
|
3
5
|
A compact [Notation3 (N3)](https://notation3.org/) reasoner in **JavaScript**.
|
|
4
6
|
|
|
5
7
|
- Single self-contained bundle (`eyeling.js`), no external runtime dependencies
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# =================================================================
|
|
2
|
+
# More advanced jsonterm example.
|
|
3
|
+
#
|
|
4
|
+
# Mapping used:
|
|
5
|
+
# - JSON objects become anonymous node structures.
|
|
6
|
+
# - JSON arrays become N3 lists.
|
|
7
|
+
# - JSON null becomes a normal symbol here, j:null.
|
|
8
|
+
# - JSON booleans become ordinary boolean literals: true and false.
|
|
9
|
+
# =================================================================
|
|
10
|
+
|
|
11
|
+
@prefix ex: <https://example.org/#> .
|
|
12
|
+
@prefix j: <https://example.org/json#> .
|
|
13
|
+
|
|
14
|
+
[
|
|
15
|
+
j:id "user-101";
|
|
16
|
+
j:profile [
|
|
17
|
+
j:name "Alice";
|
|
18
|
+
j:active true;
|
|
19
|
+
j:address [
|
|
20
|
+
j:city "Ghent";
|
|
21
|
+
j:country "BE"
|
|
22
|
+
]
|
|
23
|
+
];
|
|
24
|
+
j:roles ("admin" "editor");
|
|
25
|
+
j:preferences [
|
|
26
|
+
j:languages ("nl" "fr" "en");
|
|
27
|
+
j:theme "dark";
|
|
28
|
+
j:beta false
|
|
29
|
+
];
|
|
30
|
+
j:orders (
|
|
31
|
+
[
|
|
32
|
+
j:id "ord-1";
|
|
33
|
+
j:total 125;
|
|
34
|
+
j:items ("book" "pen")
|
|
35
|
+
]
|
|
36
|
+
[
|
|
37
|
+
j:id "ord-2";
|
|
38
|
+
j:total 45;
|
|
39
|
+
j:items ("notebook");
|
|
40
|
+
j:coupon j:null
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
] ex:kind j:UserDocument .
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
?Doc ex:kind j:UserDocument .
|
|
47
|
+
?Doc j:profile [
|
|
48
|
+
j:name ?Name;
|
|
49
|
+
j:active true;
|
|
50
|
+
j:address [ j:country "BE" ]
|
|
51
|
+
] .
|
|
52
|
+
}
|
|
53
|
+
=>
|
|
54
|
+
{
|
|
55
|
+
?Doc ex:userName ?Name .
|
|
56
|
+
?Doc ex:eligibleForReview true .
|
|
57
|
+
} .
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
?Doc j:preferences [
|
|
61
|
+
j:languages ("nl" "fr" "en");
|
|
62
|
+
j:theme "dark";
|
|
63
|
+
j:beta false
|
|
64
|
+
] .
|
|
65
|
+
}
|
|
66
|
+
=>
|
|
67
|
+
{
|
|
68
|
+
?Doc ex:profileTag "multilingual-dark-profile" .
|
|
69
|
+
} .
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
?Doc j:orders (
|
|
73
|
+
[ j:id ?FirstId; j:total 125; j:items ("book" "pen") ]
|
|
74
|
+
[ j:id ?SecondId; j:total 45; j:items ("notebook"); j:coupon j:null ]
|
|
75
|
+
) .
|
|
76
|
+
}
|
|
77
|
+
=>
|
|
78
|
+
{
|
|
79
|
+
?Doc ex:hasHighValueStarterOrder true .
|
|
80
|
+
?Doc ex:hasCouponlessFollowup true .
|
|
81
|
+
} .
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
?Doc ex:eligibleForReview true .
|
|
85
|
+
?Doc ex:profileTag "multilingual-dark-profile" .
|
|
86
|
+
?Doc ex:hasHighValueStarterOrder true .
|
|
87
|
+
?Doc ex:hasCouponlessFollowup true .
|
|
88
|
+
}
|
|
89
|
+
=>
|
|
90
|
+
{
|
|
91
|
+
ex:test ex:is true .
|
|
92
|
+
} .
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# =================================================
|
|
2
|
+
# jsonterm example
|
|
3
|
+
#
|
|
4
|
+
# Mapping used:
|
|
5
|
+
# - JSON objects become anonymous node structures.
|
|
6
|
+
# - JSON arrays become N3 lists.
|
|
7
|
+
# - JSON null becomes a normal symbol here, j:null.
|
|
8
|
+
# =================================================
|
|
9
|
+
|
|
10
|
+
@prefix ex: <https://example.org/#> .
|
|
11
|
+
@prefix j: <https://example.org/json#> .
|
|
12
|
+
|
|
13
|
+
[
|
|
14
|
+
j:user "alice";
|
|
15
|
+
j:roles ("admin" j:null)
|
|
16
|
+
]
|
|
17
|
+
("likes" 1)
|
|
18
|
+
[
|
|
19
|
+
j:city "Paris";
|
|
20
|
+
j:coords (2.35 48.85)
|
|
21
|
+
] .
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
[
|
|
25
|
+
j:user ?U;
|
|
26
|
+
j:roles ("admin" j:null)
|
|
27
|
+
]
|
|
28
|
+
("likes" 1)
|
|
29
|
+
[
|
|
30
|
+
j:city "Paris";
|
|
31
|
+
j:coords (2.35 48.85)
|
|
32
|
+
] .
|
|
33
|
+
}
|
|
34
|
+
=>
|
|
35
|
+
{
|
|
36
|
+
ex:test ex:is true .
|
|
37
|
+
} .
|
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
@prefix : <http://www.agfa.com/w3c/euler/graph.axiom#> .
|
|
2
2
|
|
|
3
|
-
:paris :path :orleans .
|
|
4
|
-
:paris :path :chartres .
|
|
5
|
-
:paris :path :amiens .
|
|
6
|
-
:orleans :path :blois .
|
|
7
|
-
:orleans :path :bourges .
|
|
8
|
-
:blois :path :tours .
|
|
9
|
-
:chartres :path :lemans .
|
|
10
|
-
:lemans :path :angers .
|
|
11
|
-
:lemans :path :tours .
|
|
12
3
|
:angers :path :nantes .
|
|
13
|
-
:paris :path :blois .
|
|
14
|
-
:paris :path :bourges .
|
|
15
|
-
:paris :path :lemans .
|
|
16
|
-
:orleans :path :tours .
|
|
17
|
-
:chartres :path :angers .
|
|
18
|
-
:chartres :path :tours .
|
|
19
|
-
:lemans :path :nantes .
|
|
20
|
-
:paris :path :tours .
|
|
21
|
-
:paris :path :angers .
|
|
22
4
|
:chartres :path :nantes .
|
|
5
|
+
:lemans :path :nantes .
|
|
23
6
|
:paris :path :nantes .
|
package/eyeling.js
CHANGED
|
@@ -5352,27 +5352,80 @@ function triplesListEqual(xs, ys) {
|
|
|
5352
5352
|
return true;
|
|
5353
5353
|
}
|
|
5354
5354
|
|
|
5355
|
-
|
|
5355
|
+
function collectProtectedNamesInTerm(t, protectedVars, protectedBlanks) {
|
|
5356
|
+
if (t instanceof Var) {
|
|
5357
|
+
protectedVars.add(t.name);
|
|
5358
|
+
return;
|
|
5359
|
+
}
|
|
5360
|
+
if (t instanceof Blank) {
|
|
5361
|
+
protectedBlanks.add(t.label);
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
if (t instanceof ListTerm) {
|
|
5365
|
+
for (const e of t.elems) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
if (t instanceof OpenListTerm) {
|
|
5369
|
+
for (const e of t.prefix) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
|
|
5370
|
+
protectedVars.add(t.tailVar);
|
|
5371
|
+
return;
|
|
5372
|
+
}
|
|
5373
|
+
if (t instanceof GraphTerm) {
|
|
5374
|
+
for (const tr of t.triples) {
|
|
5375
|
+
collectProtectedNamesInTerm(tr.s, protectedVars, protectedBlanks);
|
|
5376
|
+
collectProtectedNamesInTerm(tr.p, protectedVars, protectedBlanks);
|
|
5377
|
+
collectProtectedNamesInTerm(tr.o, protectedVars, protectedBlanks);
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
function collectProtectedNamesFromSubst(subst) {
|
|
5383
|
+
const protectedVars = new Set();
|
|
5384
|
+
const protectedBlanks = new Set();
|
|
5385
|
+
if (!subst) return { protectedVars, protectedBlanks };
|
|
5386
|
+
for (const k in subst) {
|
|
5387
|
+
if (!Object.prototype.hasOwnProperty.call(subst, k)) continue;
|
|
5388
|
+
collectProtectedNamesInTerm(subst[k], protectedVars, protectedBlanks);
|
|
5389
|
+
}
|
|
5390
|
+
return { protectedVars, protectedBlanks };
|
|
5391
|
+
}
|
|
5392
|
+
|
|
5393
|
+
// Alpha-equivalence for quoted formulas, up to *local* variable and blank-node renaming.
|
|
5394
|
+
// Terms that originate from the surrounding substitution are treated as fixed and are
|
|
5395
|
+
// therefore not alpha-renamable inside the quoted formula.
|
|
5356
5396
|
// Treats a formula as an unordered set of triples (order-insensitive match).
|
|
5357
|
-
function alphaEqVarName(x, y, vmap) {
|
|
5397
|
+
function alphaEqVarName(x, y, vmap, protectedVarsA, protectedVarsB) {
|
|
5398
|
+
const xProtected = protectedVarsA && protectedVarsA.has(x);
|
|
5399
|
+
const yProtected = protectedVarsB && protectedVarsB.has(y);
|
|
5400
|
+
if (xProtected || yProtected) return xProtected && yProtected && x === y;
|
|
5358
5401
|
if (Object.prototype.hasOwnProperty.call(vmap, x)) return vmap[x] === y;
|
|
5359
5402
|
vmap[x] = y;
|
|
5360
5403
|
return true;
|
|
5361
5404
|
}
|
|
5362
5405
|
|
|
5363
|
-
function
|
|
5364
|
-
|
|
5406
|
+
function alphaEqBlankLabel(x, y, bmap, protectedBlanksA, protectedBlanksB) {
|
|
5407
|
+
const xProtected = protectedBlanksA && protectedBlanksA.has(x);
|
|
5408
|
+
const yProtected = protectedBlanksB && protectedBlanksB.has(y);
|
|
5409
|
+
if (xProtected || yProtected) return xProtected && yProtected && x === y;
|
|
5410
|
+
if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
|
|
5411
|
+
bmap[x] = y;
|
|
5412
|
+
return true;
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
function alphaEqTermInGraph(a, b, vmap, bmap, opts) {
|
|
5416
|
+
const protectedVarsA = opts && opts.protectedVarsA;
|
|
5417
|
+
const protectedVarsB = opts && opts.protectedVarsB;
|
|
5418
|
+
const protectedBlanksA = opts && opts.protectedBlanksA;
|
|
5419
|
+
const protectedBlanksB = opts && opts.protectedBlanksB;
|
|
5420
|
+
|
|
5421
|
+
// Blank nodes: renamable only when they are local to the formula.
|
|
5365
5422
|
if (a instanceof Blank && b instanceof Blank) {
|
|
5366
|
-
|
|
5367
|
-
const y = b.label;
|
|
5368
|
-
if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
|
|
5369
|
-
bmap[x] = y;
|
|
5370
|
-
return true;
|
|
5423
|
+
return alphaEqBlankLabel(a.label, b.label, bmap, protectedBlanksA, protectedBlanksB);
|
|
5371
5424
|
}
|
|
5372
5425
|
|
|
5373
|
-
// Variables: renamable
|
|
5426
|
+
// Variables: renamable only when they are local to the formula.
|
|
5374
5427
|
if (a instanceof Var && b instanceof Var) {
|
|
5375
|
-
return alphaEqVarName(a.name, b.name, vmap);
|
|
5428
|
+
return alphaEqVarName(a.name, b.name, vmap, protectedVarsA, protectedVarsB);
|
|
5376
5429
|
}
|
|
5377
5430
|
|
|
5378
5431
|
if (a instanceof Iri && b instanceof Iri) return a.value === b.value;
|
|
@@ -5381,7 +5434,7 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
|
|
|
5381
5434
|
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
5382
5435
|
if (a.elems.length !== b.elems.length) return false;
|
|
5383
5436
|
for (let i = 0; i < a.elems.length; i++) {
|
|
5384
|
-
if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap)) return false;
|
|
5437
|
+
if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap, opts)) return false;
|
|
5385
5438
|
}
|
|
5386
5439
|
return true;
|
|
5387
5440
|
}
|
|
@@ -5389,29 +5442,30 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
|
|
|
5389
5442
|
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
5390
5443
|
if (a.prefix.length !== b.prefix.length) return false;
|
|
5391
5444
|
for (let i = 0; i < a.prefix.length; i++) {
|
|
5392
|
-
if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap)) return false;
|
|
5445
|
+
if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap, opts)) return false;
|
|
5393
5446
|
}
|
|
5394
|
-
// tailVar is a var-name string, so treat it as renamable too
|
|
5395
|
-
return alphaEqVarName(a.tailVar, b.tailVar, vmap);
|
|
5447
|
+
// tailVar is a var-name string, so treat it as renamable too when local.
|
|
5448
|
+
return alphaEqVarName(a.tailVar, b.tailVar, vmap, protectedVarsA, protectedVarsB);
|
|
5396
5449
|
}
|
|
5397
5450
|
|
|
5398
|
-
// Nested formulas: compare with fresh maps (separate scope)
|
|
5451
|
+
// Nested formulas: compare with fresh maps (separate scope), but keep the same
|
|
5452
|
+
// protected outer names so already-substituted terms stay fixed everywhere.
|
|
5399
5453
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
5400
|
-
return alphaEqGraphTriples(a.triples, b.triples);
|
|
5454
|
+
return alphaEqGraphTriples(a.triples, b.triples, opts);
|
|
5401
5455
|
}
|
|
5402
5456
|
|
|
5403
5457
|
return false;
|
|
5404
5458
|
}
|
|
5405
5459
|
|
|
5406
|
-
function alphaEqTripleInGraph(a, b, vmap, bmap) {
|
|
5460
|
+
function alphaEqTripleInGraph(a, b, vmap, bmap, opts) {
|
|
5407
5461
|
return (
|
|
5408
|
-
alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
|
|
5409
|
-
alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
|
|
5410
|
-
alphaEqTermInGraph(a.o, b.o, vmap, bmap)
|
|
5462
|
+
alphaEqTermInGraph(a.s, b.s, vmap, bmap, opts) &&
|
|
5463
|
+
alphaEqTermInGraph(a.p, b.p, vmap, bmap, opts) &&
|
|
5464
|
+
alphaEqTermInGraph(a.o, b.o, vmap, bmap, opts)
|
|
5411
5465
|
);
|
|
5412
5466
|
}
|
|
5413
5467
|
|
|
5414
|
-
function alphaEqGraphTriples(xs, ys) {
|
|
5468
|
+
function alphaEqGraphTriples(xs, ys, opts) {
|
|
5415
5469
|
if (xs.length !== ys.length) return false;
|
|
5416
5470
|
// Fast path: exact same sequence.
|
|
5417
5471
|
if (triplesListEqual(xs, ys)) return true;
|
|
@@ -5430,7 +5484,7 @@ function alphaEqGraphTriples(xs, ys) {
|
|
|
5430
5484
|
|
|
5431
5485
|
const v2 = { ...vmap };
|
|
5432
5486
|
const b2 = { ...bmap };
|
|
5433
|
-
if (!alphaEqTripleInGraph(x, y, v2, b2)) continue;
|
|
5487
|
+
if (!alphaEqTripleInGraph(x, y, v2, b2, opts)) continue;
|
|
5434
5488
|
|
|
5435
5489
|
used[j] = true;
|
|
5436
5490
|
if (step(i + 1, v2, b2)) return true;
|
|
@@ -6149,7 +6203,17 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
6149
6203
|
|
|
6150
6204
|
// Graphs
|
|
6151
6205
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
6152
|
-
|
|
6206
|
+
const protectedNames = collectProtectedNamesFromSubst(subst);
|
|
6207
|
+
if (
|
|
6208
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
6209
|
+
protectedVarsA: protectedNames.protectedVars,
|
|
6210
|
+
protectedVarsB: protectedNames.protectedVars,
|
|
6211
|
+
protectedBlanksA: protectedNames.protectedBlanks,
|
|
6212
|
+
protectedBlanksB: protectedNames.protectedBlanks,
|
|
6213
|
+
})
|
|
6214
|
+
) {
|
|
6215
|
+
return subst;
|
|
6216
|
+
}
|
|
6153
6217
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
6154
6218
|
}
|
|
6155
6219
|
|
|
@@ -6521,7 +6585,17 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
6521
6585
|
|
|
6522
6586
|
// Graphs
|
|
6523
6587
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
6524
|
-
|
|
6588
|
+
const protectedNames = collectProtectedNamesFromSubst(substMut);
|
|
6589
|
+
if (
|
|
6590
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
6591
|
+
protectedVarsA: protectedNames.protectedVars,
|
|
6592
|
+
protectedVarsB: protectedNames.protectedVars,
|
|
6593
|
+
protectedBlanksA: protectedNames.protectedBlanks,
|
|
6594
|
+
protectedBlanksB: protectedNames.protectedBlanks,
|
|
6595
|
+
})
|
|
6596
|
+
) {
|
|
6597
|
+
return true;
|
|
6598
|
+
}
|
|
6525
6599
|
// Fallback: reuse allocation-heavy graph unifier rarely hit in typical workloads.
|
|
6526
6600
|
const delta = unifyGraphTriples(a.triples, b.triples, {});
|
|
6527
6601
|
if (delta === null) return false;
|
package/lib/engine.js
CHANGED
|
@@ -736,27 +736,80 @@ function triplesListEqual(xs, ys) {
|
|
|
736
736
|
return true;
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
-
|
|
739
|
+
function collectProtectedNamesInTerm(t, protectedVars, protectedBlanks) {
|
|
740
|
+
if (t instanceof Var) {
|
|
741
|
+
protectedVars.add(t.name);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (t instanceof Blank) {
|
|
745
|
+
protectedBlanks.add(t.label);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (t instanceof ListTerm) {
|
|
749
|
+
for (const e of t.elems) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (t instanceof OpenListTerm) {
|
|
753
|
+
for (const e of t.prefix) collectProtectedNamesInTerm(e, protectedVars, protectedBlanks);
|
|
754
|
+
protectedVars.add(t.tailVar);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (t instanceof GraphTerm) {
|
|
758
|
+
for (const tr of t.triples) {
|
|
759
|
+
collectProtectedNamesInTerm(tr.s, protectedVars, protectedBlanks);
|
|
760
|
+
collectProtectedNamesInTerm(tr.p, protectedVars, protectedBlanks);
|
|
761
|
+
collectProtectedNamesInTerm(tr.o, protectedVars, protectedBlanks);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function collectProtectedNamesFromSubst(subst) {
|
|
767
|
+
const protectedVars = new Set();
|
|
768
|
+
const protectedBlanks = new Set();
|
|
769
|
+
if (!subst) return { protectedVars, protectedBlanks };
|
|
770
|
+
for (const k in subst) {
|
|
771
|
+
if (!Object.prototype.hasOwnProperty.call(subst, k)) continue;
|
|
772
|
+
collectProtectedNamesInTerm(subst[k], protectedVars, protectedBlanks);
|
|
773
|
+
}
|
|
774
|
+
return { protectedVars, protectedBlanks };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Alpha-equivalence for quoted formulas, up to *local* variable and blank-node renaming.
|
|
778
|
+
// Terms that originate from the surrounding substitution are treated as fixed and are
|
|
779
|
+
// therefore not alpha-renamable inside the quoted formula.
|
|
740
780
|
// Treats a formula as an unordered set of triples (order-insensitive match).
|
|
741
|
-
function alphaEqVarName(x, y, vmap) {
|
|
781
|
+
function alphaEqVarName(x, y, vmap, protectedVarsA, protectedVarsB) {
|
|
782
|
+
const xProtected = protectedVarsA && protectedVarsA.has(x);
|
|
783
|
+
const yProtected = protectedVarsB && protectedVarsB.has(y);
|
|
784
|
+
if (xProtected || yProtected) return xProtected && yProtected && x === y;
|
|
742
785
|
if (Object.prototype.hasOwnProperty.call(vmap, x)) return vmap[x] === y;
|
|
743
786
|
vmap[x] = y;
|
|
744
787
|
return true;
|
|
745
788
|
}
|
|
746
789
|
|
|
747
|
-
function
|
|
748
|
-
|
|
790
|
+
function alphaEqBlankLabel(x, y, bmap, protectedBlanksA, protectedBlanksB) {
|
|
791
|
+
const xProtected = protectedBlanksA && protectedBlanksA.has(x);
|
|
792
|
+
const yProtected = protectedBlanksB && protectedBlanksB.has(y);
|
|
793
|
+
if (xProtected || yProtected) return xProtected && yProtected && x === y;
|
|
794
|
+
if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
|
|
795
|
+
bmap[x] = y;
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function alphaEqTermInGraph(a, b, vmap, bmap, opts) {
|
|
800
|
+
const protectedVarsA = opts && opts.protectedVarsA;
|
|
801
|
+
const protectedVarsB = opts && opts.protectedVarsB;
|
|
802
|
+
const protectedBlanksA = opts && opts.protectedBlanksA;
|
|
803
|
+
const protectedBlanksB = opts && opts.protectedBlanksB;
|
|
804
|
+
|
|
805
|
+
// Blank nodes: renamable only when they are local to the formula.
|
|
749
806
|
if (a instanceof Blank && b instanceof Blank) {
|
|
750
|
-
|
|
751
|
-
const y = b.label;
|
|
752
|
-
if (Object.prototype.hasOwnProperty.call(bmap, x)) return bmap[x] === y;
|
|
753
|
-
bmap[x] = y;
|
|
754
|
-
return true;
|
|
807
|
+
return alphaEqBlankLabel(a.label, b.label, bmap, protectedBlanksA, protectedBlanksB);
|
|
755
808
|
}
|
|
756
809
|
|
|
757
|
-
// Variables: renamable
|
|
810
|
+
// Variables: renamable only when they are local to the formula.
|
|
758
811
|
if (a instanceof Var && b instanceof Var) {
|
|
759
|
-
return alphaEqVarName(a.name, b.name, vmap);
|
|
812
|
+
return alphaEqVarName(a.name, b.name, vmap, protectedVarsA, protectedVarsB);
|
|
760
813
|
}
|
|
761
814
|
|
|
762
815
|
if (a instanceof Iri && b instanceof Iri) return a.value === b.value;
|
|
@@ -765,7 +818,7 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
|
|
|
765
818
|
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
766
819
|
if (a.elems.length !== b.elems.length) return false;
|
|
767
820
|
for (let i = 0; i < a.elems.length; i++) {
|
|
768
|
-
if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap)) return false;
|
|
821
|
+
if (!alphaEqTermInGraph(a.elems[i], b.elems[i], vmap, bmap, opts)) return false;
|
|
769
822
|
}
|
|
770
823
|
return true;
|
|
771
824
|
}
|
|
@@ -773,29 +826,30 @@ function alphaEqTermInGraph(a, b, vmap, bmap) {
|
|
|
773
826
|
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
774
827
|
if (a.prefix.length !== b.prefix.length) return false;
|
|
775
828
|
for (let i = 0; i < a.prefix.length; i++) {
|
|
776
|
-
if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap)) return false;
|
|
829
|
+
if (!alphaEqTermInGraph(a.prefix[i], b.prefix[i], vmap, bmap, opts)) return false;
|
|
777
830
|
}
|
|
778
|
-
// tailVar is a var-name string, so treat it as renamable too
|
|
779
|
-
return alphaEqVarName(a.tailVar, b.tailVar, vmap);
|
|
831
|
+
// tailVar is a var-name string, so treat it as renamable too when local.
|
|
832
|
+
return alphaEqVarName(a.tailVar, b.tailVar, vmap, protectedVarsA, protectedVarsB);
|
|
780
833
|
}
|
|
781
834
|
|
|
782
|
-
// Nested formulas: compare with fresh maps (separate scope)
|
|
835
|
+
// Nested formulas: compare with fresh maps (separate scope), but keep the same
|
|
836
|
+
// protected outer names so already-substituted terms stay fixed everywhere.
|
|
783
837
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
784
|
-
return alphaEqGraphTriples(a.triples, b.triples);
|
|
838
|
+
return alphaEqGraphTriples(a.triples, b.triples, opts);
|
|
785
839
|
}
|
|
786
840
|
|
|
787
841
|
return false;
|
|
788
842
|
}
|
|
789
843
|
|
|
790
|
-
function alphaEqTripleInGraph(a, b, vmap, bmap) {
|
|
844
|
+
function alphaEqTripleInGraph(a, b, vmap, bmap, opts) {
|
|
791
845
|
return (
|
|
792
|
-
alphaEqTermInGraph(a.s, b.s, vmap, bmap) &&
|
|
793
|
-
alphaEqTermInGraph(a.p, b.p, vmap, bmap) &&
|
|
794
|
-
alphaEqTermInGraph(a.o, b.o, vmap, bmap)
|
|
846
|
+
alphaEqTermInGraph(a.s, b.s, vmap, bmap, opts) &&
|
|
847
|
+
alphaEqTermInGraph(a.p, b.p, vmap, bmap, opts) &&
|
|
848
|
+
alphaEqTermInGraph(a.o, b.o, vmap, bmap, opts)
|
|
795
849
|
);
|
|
796
850
|
}
|
|
797
851
|
|
|
798
|
-
function alphaEqGraphTriples(xs, ys) {
|
|
852
|
+
function alphaEqGraphTriples(xs, ys, opts) {
|
|
799
853
|
if (xs.length !== ys.length) return false;
|
|
800
854
|
// Fast path: exact same sequence.
|
|
801
855
|
if (triplesListEqual(xs, ys)) return true;
|
|
@@ -814,7 +868,7 @@ function alphaEqGraphTriples(xs, ys) {
|
|
|
814
868
|
|
|
815
869
|
const v2 = { ...vmap };
|
|
816
870
|
const b2 = { ...bmap };
|
|
817
|
-
if (!alphaEqTripleInGraph(x, y, v2, b2)) continue;
|
|
871
|
+
if (!alphaEqTripleInGraph(x, y, v2, b2, opts)) continue;
|
|
818
872
|
|
|
819
873
|
used[j] = true;
|
|
820
874
|
if (step(i + 1, v2, b2)) return true;
|
|
@@ -1533,7 +1587,17 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1533
1587
|
|
|
1534
1588
|
// Graphs
|
|
1535
1589
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
1536
|
-
|
|
1590
|
+
const protectedNames = collectProtectedNamesFromSubst(subst);
|
|
1591
|
+
if (
|
|
1592
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
1593
|
+
protectedVarsA: protectedNames.protectedVars,
|
|
1594
|
+
protectedVarsB: protectedNames.protectedVars,
|
|
1595
|
+
protectedBlanksA: protectedNames.protectedBlanks,
|
|
1596
|
+
protectedBlanksB: protectedNames.protectedBlanks,
|
|
1597
|
+
})
|
|
1598
|
+
) {
|
|
1599
|
+
return subst;
|
|
1600
|
+
}
|
|
1537
1601
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
1538
1602
|
}
|
|
1539
1603
|
|
|
@@ -1905,7 +1969,17 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
1905
1969
|
|
|
1906
1970
|
// Graphs
|
|
1907
1971
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
1908
|
-
|
|
1972
|
+
const protectedNames = collectProtectedNamesFromSubst(substMut);
|
|
1973
|
+
if (
|
|
1974
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
1975
|
+
protectedVarsA: protectedNames.protectedVars,
|
|
1976
|
+
protectedVarsB: protectedNames.protectedVars,
|
|
1977
|
+
protectedBlanksA: protectedNames.protectedBlanks,
|
|
1978
|
+
protectedBlanksB: protectedNames.protectedBlanks,
|
|
1979
|
+
})
|
|
1980
|
+
) {
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1909
1983
|
// Fallback: reuse allocation-heavy graph unifier rarely hit in typical workloads.
|
|
1910
1984
|
const delta = unifyGraphTriples(a.triples, b.triples, {});
|
|
1911
1985
|
if (delta === null) return false;
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -1432,6 +1432,34 @@ ex:w a ex:Woman .
|
|
|
1432
1432
|
`,
|
|
1433
1433
|
expect: [/:(?:test)\s+:(?:is)\s+true\s*\./],
|
|
1434
1434
|
},
|
|
1435
|
+
|
|
1436
|
+
{
|
|
1437
|
+
name: '59 regression: quoted-formula alpha-equivalence must not rename blanks introduced by outer substitution',
|
|
1438
|
+
opt: { proofComments: false },
|
|
1439
|
+
input: `@prefix : <http://example.org/> .
|
|
1440
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
1441
|
+
@prefix math: <http://www.w3.org/2000/10/swap/math#> .
|
|
1442
|
+
|
|
1443
|
+
_:x :hates { _:foo :making :mess }.
|
|
1444
|
+
|
|
1445
|
+
{
|
|
1446
|
+
?A :hates { ?A :making :mess }.
|
|
1447
|
+
}
|
|
1448
|
+
=>
|
|
1449
|
+
{
|
|
1450
|
+
?A :hates :Himself.
|
|
1451
|
+
}.
|
|
1452
|
+
|
|
1453
|
+
{
|
|
1454
|
+
?A :hates :Himself.
|
|
1455
|
+
}
|
|
1456
|
+
=>
|
|
1457
|
+
{
|
|
1458
|
+
:test :is false.
|
|
1459
|
+
}.
|
|
1460
|
+
`,
|
|
1461
|
+
notExpect: [/:(?:test)\s+:(?:is)\s+false\s*\./],
|
|
1462
|
+
},
|
|
1435
1463
|
];
|
|
1436
1464
|
|
|
1437
1465
|
let passed = 0;
|