eyeling 1.22.13 → 1.22.14

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
@@ -436,6 +436,8 @@ Important scope nuance: only blanks/variables that are local to the quoted formu
436
436
 
437
437
  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`.
438
438
 
439
+ A related operational detail matters for rule execution: alpha-equivalence is only a **binding-free shortcut** when both quoted formulas are variable-free after substitution. If unbound variables still remain inside the formulas, Eyeling must fall back to structural quoted-formula unification so shared outer rule variables can actually bind. Otherwise a premise such as `?A :has { ?S ?P ?O }` could appear to match while leaving `?S ?P ?O` unbound for later goals.
440
+
439
441
  ### 6.2 Groundness: “variables inside formulas do not leak”
440
442
 
441
443
  Eyeling makes a deliberate choice about _groundness_:
@@ -7625,6 +7625,22 @@
7625
7625
  return s2;
7626
7626
  }
7627
7627
 
7628
+ function graphTriplesContainVars(triples) {
7629
+ function termHasVar(t) {
7630
+ if (t instanceof Var) return true;
7631
+ if (t instanceof ListTerm) return t.elems.some(termHasVar);
7632
+ if (t instanceof OpenListTerm) return t.prefix.some(termHasVar) || true;
7633
+ if (t instanceof GraphTerm)
7634
+ return t.triples.some((tr) => termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o));
7635
+ return false;
7636
+ }
7637
+
7638
+ for (const tr of triples) {
7639
+ if (termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o)) return true;
7640
+ }
7641
+ return false;
7642
+ }
7643
+
7628
7644
  function unifyGraphTriples(xs, ys, subst) {
7629
7645
  if (xs.length !== ys.length) return null;
7630
7646
 
@@ -7786,17 +7802,22 @@
7786
7802
 
7787
7803
  // Graphs
7788
7804
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
7789
- const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
7790
- const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
7791
- if (
7792
- alphaEqGraphTriples(a.triples, b.triples, {
7793
- protectedVarsA: protectedNamesA.protectedVars,
7794
- protectedVarsB: protectedNamesB.protectedVars,
7795
- protectedBlanksA: protectedNamesA.protectedBlanks,
7796
- protectedBlanksB: protectedNamesB.protectedBlanks,
7797
- })
7798
- ) {
7799
- return subst;
7805
+ // Only use alpha-equivalence as a binding-free fast path when both quoted
7806
+ // formulas are variable-free after substitution. If unbound variables remain,
7807
+ // they may be shared with the outer rule and must unify/bind structurally.
7808
+ if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
7809
+ const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
7810
+ const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
7811
+ if (
7812
+ alphaEqGraphTriples(a.triples, b.triples, {
7813
+ protectedVarsA: protectedNamesA.protectedVars,
7814
+ protectedVarsB: protectedNamesB.protectedVars,
7815
+ protectedBlanksA: protectedNamesA.protectedBlanks,
7816
+ protectedBlanksB: protectedNamesB.protectedBlanks,
7817
+ })
7818
+ ) {
7819
+ return subst;
7820
+ }
7800
7821
  }
7801
7822
  return unifyGraphTriples(a.triples, b.triples, subst);
7802
7823
  }
@@ -8177,17 +8198,22 @@
8177
8198
 
8178
8199
  // Graphs
8179
8200
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
8180
- const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
8181
- const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
8182
- if (
8183
- alphaEqGraphTriples(a.triples, b.triples, {
8184
- protectedVarsA: protectedNamesA.protectedVars,
8185
- protectedVarsB: protectedNamesB.protectedVars,
8186
- protectedBlanksA: protectedNamesA.protectedBlanks,
8187
- protectedBlanksB: protectedNamesB.protectedBlanks,
8188
- })
8189
- ) {
8190
- return true;
8201
+ // Only use alpha-equivalence as a binding-free fast path when both quoted
8202
+ // formulas are variable-free after substitution. If unbound variables remain,
8203
+ // they may be shared with the outer rule and must unify/bind structurally.
8204
+ if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
8205
+ const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
8206
+ const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
8207
+ if (
8208
+ alphaEqGraphTriples(a.triples, b.triples, {
8209
+ protectedVarsA: protectedNamesA.protectedVars,
8210
+ protectedVarsB: protectedNamesB.protectedVars,
8211
+ protectedBlanksA: protectedNamesA.protectedBlanks,
8212
+ protectedBlanksB: protectedNamesB.protectedBlanks,
8213
+ })
8214
+ ) {
8215
+ return true;
8216
+ }
8191
8217
  }
8192
8218
  const merged = unifyGraphTriples(a.triples, b.triples, substMut);
8193
8219
  if (merged === null) return false;
package/eyeling.js CHANGED
@@ -7605,6 +7605,22 @@ function unifyOpenWithList(prefix, tailv, ys, subst) {
7605
7605
  return s2;
7606
7606
  }
7607
7607
 
7608
+
7609
+ function graphTriplesContainVars(triples) {
7610
+ function termHasVar(t) {
7611
+ if (t instanceof Var) return true;
7612
+ if (t instanceof ListTerm) return t.elems.some(termHasVar);
7613
+ if (t instanceof OpenListTerm) return t.prefix.some(termHasVar) || true;
7614
+ if (t instanceof GraphTerm) return t.triples.some((tr) => termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o));
7615
+ return false;
7616
+ }
7617
+
7618
+ for (const tr of triples) {
7619
+ if (termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o)) return true;
7620
+ }
7621
+ return false;
7622
+ }
7623
+
7608
7624
  function unifyGraphTriples(xs, ys, subst) {
7609
7625
  if (xs.length !== ys.length) return null;
7610
7626
 
@@ -7766,17 +7782,22 @@ function unifyTermWithOptions(a, b, subst, opts) {
7766
7782
 
7767
7783
  // Graphs
7768
7784
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
7769
- const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
7770
- const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
7771
- if (
7772
- alphaEqGraphTriples(a.triples, b.triples, {
7773
- protectedVarsA: protectedNamesA.protectedVars,
7774
- protectedVarsB: protectedNamesB.protectedVars,
7775
- protectedBlanksA: protectedNamesA.protectedBlanks,
7776
- protectedBlanksB: protectedNamesB.protectedBlanks,
7777
- })
7778
- ) {
7779
- return subst;
7785
+ // Only use alpha-equivalence as a binding-free fast path when both quoted
7786
+ // formulas are variable-free after substitution. If unbound variables remain,
7787
+ // they may be shared with the outer rule and must unify/bind structurally.
7788
+ if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
7789
+ const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
7790
+ const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
7791
+ if (
7792
+ alphaEqGraphTriples(a.triples, b.triples, {
7793
+ protectedVarsA: protectedNamesA.protectedVars,
7794
+ protectedVarsB: protectedNamesB.protectedVars,
7795
+ protectedBlanksA: protectedNamesA.protectedBlanks,
7796
+ protectedBlanksB: protectedNamesB.protectedBlanks,
7797
+ })
7798
+ ) {
7799
+ return subst;
7800
+ }
7780
7801
  }
7781
7802
  return unifyGraphTriples(a.triples, b.triples, subst);
7782
7803
  }
@@ -8157,17 +8178,22 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
8157
8178
 
8158
8179
  // Graphs
8159
8180
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
8160
- const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
8161
- const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
8162
- if (
8163
- alphaEqGraphTriples(a.triples, b.triples, {
8164
- protectedVarsA: protectedNamesA.protectedVars,
8165
- protectedVarsB: protectedNamesB.protectedVars,
8166
- protectedBlanksA: protectedNamesA.protectedBlanks,
8167
- protectedBlanksB: protectedNamesB.protectedBlanks,
8168
- })
8169
- ) {
8170
- return true;
8181
+ // Only use alpha-equivalence as a binding-free fast path when both quoted
8182
+ // formulas are variable-free after substitution. If unbound variables remain,
8183
+ // they may be shared with the outer rule and must unify/bind structurally.
8184
+ if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
8185
+ const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
8186
+ const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
8187
+ if (
8188
+ alphaEqGraphTriples(a.triples, b.triples, {
8189
+ protectedVarsA: protectedNamesA.protectedVars,
8190
+ protectedVarsB: protectedNamesB.protectedVars,
8191
+ protectedBlanksA: protectedNamesA.protectedBlanks,
8192
+ protectedBlanksB: protectedNamesB.protectedBlanks,
8193
+ })
8194
+ ) {
8195
+ return true;
8196
+ }
8171
8197
  }
8172
8198
  const merged = unifyGraphTriples(a.triples, b.triples, substMut);
8173
8199
  if (merged === null) return false;
@@ -13004,7 +13030,8 @@ function liftBlankRuleVars(premise, conclusion) {
13004
13030
  if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertQuotedPatternTerm), t.tailVar);
13005
13031
  if (t instanceof GraphTerm) {
13006
13032
  const triples = t.triples.map(
13007
- (tr) => new Triple(convertQuotedPatternTerm(tr.s), convertQuotedPatternTerm(tr.p), convertQuotedPatternTerm(tr.o)),
13033
+ (tr) =>
13034
+ new Triple(convertQuotedPatternTerm(tr.s), convertQuotedPatternTerm(tr.p), convertQuotedPatternTerm(tr.o)),
13008
13035
  );
13009
13036
  return copyQuotedGraphMetadata(t, new GraphTerm(triples));
13010
13037
  }
@@ -13014,13 +13041,17 @@ function liftBlankRuleVars(premise, conclusion) {
13014
13041
  function convertTerm(t, allowDirectQuotedPattern = false) {
13015
13042
  if (t instanceof Blank) return blankToVar(t.label);
13016
13043
  if (t instanceof ListTerm) return new ListTerm(t.elems.map((e) => convertTerm(e, false)));
13017
- if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map((e) => convertTerm(e, false)), t.tailVar);
13044
+ if (t instanceof OpenListTerm)
13045
+ return new OpenListTerm(
13046
+ t.prefix.map((e) => convertTerm(e, false)),
13047
+ t.tailVar,
13048
+ );
13018
13049
  if (t instanceof GraphTerm) return allowDirectQuotedPattern ? convertQuotedPatternTerm(t) : copyQuotedTerm(t);
13019
13050
  return t;
13020
13051
  }
13021
13052
 
13022
- const newPremise = premise.map((tr) =>
13023
- new Triple(convertTerm(tr.s, true), convertTerm(tr.p, true), convertTerm(tr.o, true)),
13053
+ const newPremise = premise.map(
13054
+ (tr) => new Triple(convertTerm(tr.s, true), convertTerm(tr.p, true), convertTerm(tr.o, true)),
13024
13055
  );
13025
13056
  return [newPremise, conclusion];
13026
13057
  }
package/lib/engine.js CHANGED
@@ -1795,6 +1795,21 @@ function unifyOpenWithList(prefix, tailv, ys, subst) {
1795
1795
  return s2;
1796
1796
  }
1797
1797
 
1798
+ function graphTriplesContainVars(triples) {
1799
+ function termHasVar(t) {
1800
+ if (t instanceof Var) return true;
1801
+ if (t instanceof ListTerm) return t.elems.some(termHasVar);
1802
+ if (t instanceof OpenListTerm) return t.prefix.some(termHasVar) || true;
1803
+ if (t instanceof GraphTerm) return t.triples.some((tr) => termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o));
1804
+ return false;
1805
+ }
1806
+
1807
+ for (const tr of triples) {
1808
+ if (termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o)) return true;
1809
+ }
1810
+ return false;
1811
+ }
1812
+
1798
1813
  function unifyGraphTriples(xs, ys, subst) {
1799
1814
  if (xs.length !== ys.length) return null;
1800
1815
 
@@ -1956,17 +1971,22 @@ function unifyTermWithOptions(a, b, subst, opts) {
1956
1971
 
1957
1972
  // Graphs
1958
1973
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
1959
- const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
1960
- const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
1961
- if (
1962
- alphaEqGraphTriples(a.triples, b.triples, {
1963
- protectedVarsA: protectedNamesA.protectedVars,
1964
- protectedVarsB: protectedNamesB.protectedVars,
1965
- protectedBlanksA: protectedNamesA.protectedBlanks,
1966
- protectedBlanksB: protectedNamesB.protectedBlanks,
1967
- })
1968
- ) {
1969
- return subst;
1974
+ // Only use alpha-equivalence as a binding-free fast path when both quoted
1975
+ // formulas are variable-free after substitution. If unbound variables remain,
1976
+ // they may be shared with the outer rule and must unify/bind structurally.
1977
+ if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
1978
+ const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
1979
+ const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
1980
+ if (
1981
+ alphaEqGraphTriples(a.triples, b.triples, {
1982
+ protectedVarsA: protectedNamesA.protectedVars,
1983
+ protectedVarsB: protectedNamesB.protectedVars,
1984
+ protectedBlanksA: protectedNamesA.protectedBlanks,
1985
+ protectedBlanksB: protectedNamesB.protectedBlanks,
1986
+ })
1987
+ ) {
1988
+ return subst;
1989
+ }
1970
1990
  }
1971
1991
  return unifyGraphTriples(a.triples, b.triples, subst);
1972
1992
  }
@@ -2347,17 +2367,22 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
2347
2367
 
2348
2368
  // Graphs
2349
2369
  if (a instanceof GraphTerm && b instanceof GraphTerm) {
2350
- const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
2351
- const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
2352
- if (
2353
- alphaEqGraphTriples(a.triples, b.triples, {
2354
- protectedVarsA: protectedNamesA.protectedVars,
2355
- protectedVarsB: protectedNamesB.protectedVars,
2356
- protectedBlanksA: protectedNamesA.protectedBlanks,
2357
- protectedBlanksB: protectedNamesB.protectedBlanks,
2358
- })
2359
- ) {
2360
- return true;
2370
+ // Only use alpha-equivalence as a binding-free fast path when both quoted
2371
+ // formulas are variable-free after substitution. If unbound variables remain,
2372
+ // they may be shared with the outer rule and must unify/bind structurally.
2373
+ if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
2374
+ const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
2375
+ const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
2376
+ if (
2377
+ alphaEqGraphTriples(a.triples, b.triples, {
2378
+ protectedVarsA: protectedNamesA.protectedVars,
2379
+ protectedVarsB: protectedNamesB.protectedVars,
2380
+ protectedBlanksA: protectedNamesA.protectedBlanks,
2381
+ protectedBlanksB: protectedNamesB.protectedBlanks,
2382
+ })
2383
+ ) {
2384
+ return true;
2385
+ }
2361
2386
  }
2362
2387
  const merged = unifyGraphTriples(a.triples, b.triples, substMut);
2363
2388
  if (merged === null) return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.22.13",
3
+ "version": "1.22.14",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -2278,6 +2278,34 @@ _:b a ex:Person ; ex:name "B" .
2278
2278
  {
2279
2279
  :test :is true .
2280
2280
  }.
2281
+ `,
2282
+ expect: [/^:test\s+:is\s+true\s*\./m],
2283
+ },
2284
+ {
2285
+ name: 'regression: quoted-formula backward-rule heads must bind shared variables for later goals',
2286
+ opt: { proofComments: false },
2287
+ input: `@prefix : <http://example.org/> .
2288
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2289
+
2290
+ { :a :b :c } a :Statement .
2291
+
2292
+ { ?A :has { ?S ?P ?O } }
2293
+ <=
2294
+ {
2295
+ ?A log:includes { ?S ?P ?O }.
2296
+ }.
2297
+
2298
+ {
2299
+ ?A a :Statement .
2300
+ ?A :has { ?S ?P ?O }.
2301
+ ?S log:rawType log:Other.
2302
+ ?P log:rawType log:Other.
2303
+ ?O log:rawType log:Other.
2304
+ }
2305
+ =>
2306
+ {
2307
+ :test :is true .
2308
+ }.
2281
2309
  `,
2282
2310
  expect: [/^:test\s+:is\s+true\s*\./m],
2283
2311
  },