eyeling 1.24.24 → 1.24.25

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.
@@ -1691,15 +1691,85 @@ function __listElemsForBuiltin(listLike, facts) {
1691
1691
  return null;
1692
1692
  }
1693
1693
 
1694
- function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
1695
- if (!(sTerm instanceof ListTerm)) return [];
1696
- if (!sTerm.elems.length) return [];
1697
- const first = sTerm.elems[0];
1698
- const s2 = unifyTerm(oTerm, first, subst);
1699
- return s2 !== null ? [s2] : [];
1694
+ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
1695
+ if (delta === null) return false;
1696
+ out.push(delta);
1697
+ return typeof maxResults === 'number' && maxResults > 0 && out.length >= maxResults;
1700
1698
  }
1701
1699
 
1702
- function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
1700
+ function __collectListLikeTermsFromTerm(t, out, seen) {
1701
+ if (t instanceof ListTerm || t instanceof OpenListTerm) {
1702
+ const k = termFastKey ? termFastKey(t) : null;
1703
+ if (k === null || !seen.has(k)) {
1704
+ if (k !== null) seen.add(k);
1705
+ out.push(t);
1706
+ }
1707
+ if (t instanceof ListTerm) {
1708
+ for (const e of t.elems) __collectListLikeTermsFromTerm(e, out, seen);
1709
+ } else {
1710
+ for (const e of t.prefix) __collectListLikeTermsFromTerm(e, out, seen);
1711
+ }
1712
+ return;
1713
+ }
1714
+ if (t instanceof GraphTerm) {
1715
+ for (const tr of t.triples) {
1716
+ __collectListLikeTermsFromTerm(tr.s, out, seen);
1717
+ __collectListLikeTermsFromTerm(tr.p, out, seen);
1718
+ __collectListLikeTermsFromTerm(tr.o, out, seen);
1719
+ }
1720
+ }
1721
+ }
1722
+
1723
+ function __collectListLikeTermsFromFacts(facts) {
1724
+ const out = [];
1725
+ const seen = new Set();
1726
+ for (const tr of facts) {
1727
+ __collectListLikeTermsFromTerm(tr.s, out, seen);
1728
+ __collectListLikeTermsFromTerm(tr.o, out, seen);
1729
+ }
1730
+ return out;
1731
+ }
1732
+
1733
+ function evalListFirstLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
1734
+ if (sTerm instanceof ListTerm) {
1735
+ if (!sTerm.elems.length) return [];
1736
+ const first = sTerm.elems[0];
1737
+ const s2 = unifyTerm(oTerm, first, subst);
1738
+ return s2 !== null ? [s2] : [];
1739
+ }
1740
+
1741
+ // For a variable subject, enumerate already-existing collection terms.
1742
+ // This lets a rule body such as `_:x rdf:first 1; rdf:rest rdf:nil`
1743
+ // bind `_:x` to an N3 collection literal `(1)` used elsewhere as a term.
1744
+ // Also include ordinary rdf:first facts so the builtin path does not hide
1745
+ // RDF-serialized lists when the subject starts unbound.
1746
+ if (sTerm instanceof Var) {
1747
+ const out = [];
1748
+
1749
+ for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
1750
+ if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
1751
+ let s2 = unifyTerm(sTerm, listTerm, subst);
1752
+ if (s2 === null) continue;
1753
+ s2 = unifyTerm(oTerm, listTerm.elems[0], s2);
1754
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1755
+ }
1756
+
1757
+ const RDF_FIRST = RDF_NS + 'first';
1758
+ for (const tr of facts) {
1759
+ if (!(tr.p instanceof Iri) || tr.p.value !== RDF_FIRST) continue;
1760
+ let s2 = unifyTerm(sTerm, tr.s, subst);
1761
+ if (s2 === null) continue;
1762
+ s2 = unifyTerm(oTerm, tr.o, s2);
1763
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1764
+ }
1765
+
1766
+ return out;
1767
+ }
1768
+
1769
+ return [];
1770
+ }
1771
+
1772
+ function evalListRestLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
1703
1773
  // Closed list: (a b c) -> (b c)
1704
1774
  if (sTerm instanceof ListTerm) {
1705
1775
  if (!sTerm.elems.length) return [];
@@ -1720,6 +1790,32 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
1720
1790
  return s2 !== null ? [s2] : [];
1721
1791
  }
1722
1792
 
1793
+ // See evalListFirstLikeBuiltin(): if the collection subject is still a
1794
+ // variable, enumerate known list literals and ordinary rdf:rest facts.
1795
+ if (sTerm instanceof Var) {
1796
+ const out = [];
1797
+
1798
+ for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
1799
+ if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
1800
+ let s2 = unifyTerm(sTerm, listTerm, subst);
1801
+ if (s2 === null) continue;
1802
+ const rest = new ListTerm(listTerm.elems.slice(1));
1803
+ s2 = unifyTerm(oTerm, rest, s2);
1804
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1805
+ }
1806
+
1807
+ const RDF_REST = RDF_NS + 'rest';
1808
+ for (const tr of facts) {
1809
+ if (!(tr.p instanceof Iri) || tr.p.value !== RDF_REST) continue;
1810
+ let s2 = unifyTerm(sTerm, tr.s, subst);
1811
+ if (s2 === null) continue;
1812
+ s2 = unifyTerm(oTerm, tr.o, s2);
1813
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1814
+ }
1815
+
1816
+ return out;
1817
+ }
1818
+
1723
1819
  return [];
1724
1820
  }
1725
1821
 
@@ -2859,14 +2955,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2859
2955
  return s2 !== null ? [s2] : [];
2860
2956
  }
2861
2957
  if (pv === RDF_NS + 'first') {
2862
- return evalListFirstLikeBuiltin(g.s, g.o, subst);
2958
+ return evalListFirstLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2863
2959
  }
2864
2960
 
2865
2961
  // list:rest and rdf:rest
2866
2962
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
2867
2963
  // Schema: $s+ list:rest $o-
2868
2964
  if (pv === LIST_NS + 'rest') {
2869
- if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
2965
+ if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2870
2966
  const xs = __listElemsForBuiltin(g.s, facts);
2871
2967
  if (!xs || !xs.length) return [];
2872
2968
  const rest = new ListTerm(xs.slice(1));
@@ -2874,7 +2970,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2874
2970
  return s2 !== null ? [s2] : [];
2875
2971
  }
2876
2972
  if (pv === RDF_NS + 'rest') {
2877
- return evalListRestLikeBuiltin(g.s, g.o, subst);
2973
+ return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2878
2974
  }
2879
2975
 
2880
2976
  // list:iterate
@@ -7918,7 +8014,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
7918
8014
  const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
7919
8015
  const shouldTreatAsBuiltin =
7920
8016
  isBuiltinPred(goal0.p) &&
7921
- !(isRdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
8017
+ !(
8018
+ isRdfFirstOrRest &&
8019
+ !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm || goal0.s instanceof Var)
8020
+ );
7922
8021
 
7923
8022
  if (shouldTreatAsBuiltin) {
7924
8023
  const remaining = max - results.length;
@@ -11703,14 +11802,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
11703
11802
  const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
11704
11803
  const RDF_JSON_DT = RDF_NS + 'JSON';
11705
11804
 
11805
+ function parseUriReferenceForResolution(uri) {
11806
+ // RFC 3986 Appendix B-style component parser, with the scheme tightened to
11807
+ // the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
11808
+ // empty query is defined, while no `?` means undefined.
11809
+ const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
11810
+ if (!m) return null;
11811
+ return {
11812
+ scheme: m[2] !== undefined ? m[2] : undefined,
11813
+ authority: m[4] !== undefined ? m[4] : undefined,
11814
+ path: m[5] || '',
11815
+ query: m[6] !== undefined ? (m[7] || '') : undefined,
11816
+ fragment: m[8] !== undefined ? (m[9] || '') : undefined,
11817
+ };
11818
+ }
11819
+
11820
+ function recomposeUriReference(parts) {
11821
+ let out = '';
11822
+ if (parts.scheme !== undefined) out += `${parts.scheme}:`;
11823
+ if (parts.authority !== undefined) out += `//${parts.authority}`;
11824
+ out += parts.path || '';
11825
+ if (parts.query !== undefined) out += `?${parts.query}`;
11826
+ if (parts.fragment !== undefined) out += `#${parts.fragment}`;
11827
+ return out;
11828
+ }
11829
+
11830
+ function removeLastPathSegment(path) {
11831
+ if (!path) return '';
11832
+ const i = path.lastIndexOf('/');
11833
+ if (i < 0) return '';
11834
+ if (i === 0) return '';
11835
+ return path.slice(0, i);
11836
+ }
11837
+
11838
+ function removeDotSegments(path) {
11839
+ // RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
11840
+ // Eyeling preserves IRI spelling (for example, it does not add a trailing
11841
+ // slash to `http://example.org`) while still normalizing `.` and `..` path
11842
+ // segments as required by section 5.2.2.
11843
+ let input = String(path || '');
11844
+ let output = '';
11845
+
11846
+ while (input.length > 0) {
11847
+ if (input.startsWith('../')) {
11848
+ input = input.slice(3);
11849
+ } else if (input.startsWith('./')) {
11850
+ input = input.slice(2);
11851
+ } else if (input.startsWith('/./')) {
11852
+ input = `/${input.slice(3)}`;
11853
+ } else if (input === '/.') {
11854
+ input = '/';
11855
+ } else if (input.startsWith('/../')) {
11856
+ input = `/${input.slice(4)}`;
11857
+ output = removeLastPathSegment(output);
11858
+ } else if (input === '/..') {
11859
+ input = '/';
11860
+ output = removeLastPathSegment(output);
11861
+ } else if (input === '.' || input === '..') {
11862
+ input = '';
11863
+ } else {
11864
+ let segmentEnd;
11865
+ if (input[0] === '/') {
11866
+ segmentEnd = input.indexOf('/', 1);
11867
+ } else {
11868
+ segmentEnd = input.indexOf('/');
11869
+ }
11870
+
11871
+ if (segmentEnd < 0) {
11872
+ output += input;
11873
+ input = '';
11874
+ } else {
11875
+ output += input.slice(0, segmentEnd);
11876
+ input = input.slice(segmentEnd);
11877
+ }
11878
+ }
11879
+ }
11880
+
11881
+ return output;
11882
+ }
11883
+
11884
+ function mergePaths(base, refPath) {
11885
+ if (base.authority !== undefined && base.path === '') {
11886
+ return `/${refPath}`;
11887
+ }
11888
+ const i = base.path.lastIndexOf('/');
11889
+ if (i < 0) return refPath;
11890
+ return `${base.path.slice(0, i + 1)}${refPath}`;
11891
+ }
11892
+
11706
11893
  function resolveIriRef(ref, base) {
11707
- if (!base) return ref;
11708
- if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(ref)) return ref; // already absolute
11709
- try {
11710
- return new URL(ref, base).toString();
11711
- } catch {
11712
- return ref;
11894
+ const r = parseUriReferenceForResolution(ref);
11895
+ if (!r) return ref;
11896
+
11897
+ const baseParts = base ? parseUriReferenceForResolution(base) : null;
11898
+
11899
+ // Absolute references do not need a base, but RFC 3986 section 5.2.2 still
11900
+ // applies remove_dot_segments(R.path) when R.scheme is defined.
11901
+ if (r.scheme !== undefined) {
11902
+ return recomposeUriReference({
11903
+ scheme: r.scheme,
11904
+ authority: r.authority,
11905
+ path: removeDotSegments(r.path),
11906
+ query: r.query,
11907
+ fragment: r.fragment,
11908
+ });
11713
11909
  }
11910
+
11911
+ // Without a usable base, preserve relative references as written.
11912
+ if (!baseParts || baseParts.scheme === undefined) return ref;
11913
+
11914
+ const t = {
11915
+ scheme: baseParts.scheme,
11916
+ authority: undefined,
11917
+ path: '',
11918
+ query: undefined,
11919
+ fragment: r.fragment,
11920
+ };
11921
+
11922
+ if (r.authority !== undefined) {
11923
+ t.authority = r.authority;
11924
+ t.path = removeDotSegments(r.path);
11925
+ t.query = r.query;
11926
+ } else if (r.path === '') {
11927
+ t.authority = baseParts.authority;
11928
+ t.path = baseParts.path;
11929
+ t.query = r.query !== undefined ? r.query : baseParts.query;
11930
+ } else {
11931
+ t.authority = baseParts.authority;
11932
+ if (r.path.startsWith('/')) {
11933
+ t.path = removeDotSegments(r.path);
11934
+ } else {
11935
+ t.path = removeDotSegments(mergePaths(baseParts, r.path));
11936
+ }
11937
+ t.query = r.query;
11938
+ }
11939
+
11940
+ return recomposeUriReference(t);
11714
11941
  }
11715
11942
 
11716
11943
  // -----------------------------------------------------------------------------
package/eyeling.js CHANGED
@@ -1691,15 +1691,85 @@ function __listElemsForBuiltin(listLike, facts) {
1691
1691
  return null;
1692
1692
  }
1693
1693
 
1694
- function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
1695
- if (!(sTerm instanceof ListTerm)) return [];
1696
- if (!sTerm.elems.length) return [];
1697
- const first = sTerm.elems[0];
1698
- const s2 = unifyTerm(oTerm, first, subst);
1699
- return s2 !== null ? [s2] : [];
1694
+ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
1695
+ if (delta === null) return false;
1696
+ out.push(delta);
1697
+ return typeof maxResults === 'number' && maxResults > 0 && out.length >= maxResults;
1700
1698
  }
1701
1699
 
1702
- function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
1700
+ function __collectListLikeTermsFromTerm(t, out, seen) {
1701
+ if (t instanceof ListTerm || t instanceof OpenListTerm) {
1702
+ const k = termFastKey ? termFastKey(t) : null;
1703
+ if (k === null || !seen.has(k)) {
1704
+ if (k !== null) seen.add(k);
1705
+ out.push(t);
1706
+ }
1707
+ if (t instanceof ListTerm) {
1708
+ for (const e of t.elems) __collectListLikeTermsFromTerm(e, out, seen);
1709
+ } else {
1710
+ for (const e of t.prefix) __collectListLikeTermsFromTerm(e, out, seen);
1711
+ }
1712
+ return;
1713
+ }
1714
+ if (t instanceof GraphTerm) {
1715
+ for (const tr of t.triples) {
1716
+ __collectListLikeTermsFromTerm(tr.s, out, seen);
1717
+ __collectListLikeTermsFromTerm(tr.p, out, seen);
1718
+ __collectListLikeTermsFromTerm(tr.o, out, seen);
1719
+ }
1720
+ }
1721
+ }
1722
+
1723
+ function __collectListLikeTermsFromFacts(facts) {
1724
+ const out = [];
1725
+ const seen = new Set();
1726
+ for (const tr of facts) {
1727
+ __collectListLikeTermsFromTerm(tr.s, out, seen);
1728
+ __collectListLikeTermsFromTerm(tr.o, out, seen);
1729
+ }
1730
+ return out;
1731
+ }
1732
+
1733
+ function evalListFirstLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
1734
+ if (sTerm instanceof ListTerm) {
1735
+ if (!sTerm.elems.length) return [];
1736
+ const first = sTerm.elems[0];
1737
+ const s2 = unifyTerm(oTerm, first, subst);
1738
+ return s2 !== null ? [s2] : [];
1739
+ }
1740
+
1741
+ // For a variable subject, enumerate already-existing collection terms.
1742
+ // This lets a rule body such as `_:x rdf:first 1; rdf:rest rdf:nil`
1743
+ // bind `_:x` to an N3 collection literal `(1)` used elsewhere as a term.
1744
+ // Also include ordinary rdf:first facts so the builtin path does not hide
1745
+ // RDF-serialized lists when the subject starts unbound.
1746
+ if (sTerm instanceof Var) {
1747
+ const out = [];
1748
+
1749
+ for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
1750
+ if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
1751
+ let s2 = unifyTerm(sTerm, listTerm, subst);
1752
+ if (s2 === null) continue;
1753
+ s2 = unifyTerm(oTerm, listTerm.elems[0], s2);
1754
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1755
+ }
1756
+
1757
+ const RDF_FIRST = RDF_NS + 'first';
1758
+ for (const tr of facts) {
1759
+ if (!(tr.p instanceof Iri) || tr.p.value !== RDF_FIRST) continue;
1760
+ let s2 = unifyTerm(sTerm, tr.s, subst);
1761
+ if (s2 === null) continue;
1762
+ s2 = unifyTerm(oTerm, tr.o, s2);
1763
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1764
+ }
1765
+
1766
+ return out;
1767
+ }
1768
+
1769
+ return [];
1770
+ }
1771
+
1772
+ function evalListRestLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
1703
1773
  // Closed list: (a b c) -> (b c)
1704
1774
  if (sTerm instanceof ListTerm) {
1705
1775
  if (!sTerm.elems.length) return [];
@@ -1720,6 +1790,32 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
1720
1790
  return s2 !== null ? [s2] : [];
1721
1791
  }
1722
1792
 
1793
+ // See evalListFirstLikeBuiltin(): if the collection subject is still a
1794
+ // variable, enumerate known list literals and ordinary rdf:rest facts.
1795
+ if (sTerm instanceof Var) {
1796
+ const out = [];
1797
+
1798
+ for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
1799
+ if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
1800
+ let s2 = unifyTerm(sTerm, listTerm, subst);
1801
+ if (s2 === null) continue;
1802
+ const rest = new ListTerm(listTerm.elems.slice(1));
1803
+ s2 = unifyTerm(oTerm, rest, s2);
1804
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1805
+ }
1806
+
1807
+ const RDF_REST = RDF_NS + 'rest';
1808
+ for (const tr of facts) {
1809
+ if (!(tr.p instanceof Iri) || tr.p.value !== RDF_REST) continue;
1810
+ let s2 = unifyTerm(sTerm, tr.s, subst);
1811
+ if (s2 === null) continue;
1812
+ s2 = unifyTerm(oTerm, tr.o, s2);
1813
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1814
+ }
1815
+
1816
+ return out;
1817
+ }
1818
+
1723
1819
  return [];
1724
1820
  }
1725
1821
 
@@ -2859,14 +2955,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2859
2955
  return s2 !== null ? [s2] : [];
2860
2956
  }
2861
2957
  if (pv === RDF_NS + 'first') {
2862
- return evalListFirstLikeBuiltin(g.s, g.o, subst);
2958
+ return evalListFirstLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2863
2959
  }
2864
2960
 
2865
2961
  // list:rest and rdf:rest
2866
2962
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
2867
2963
  // Schema: $s+ list:rest $o-
2868
2964
  if (pv === LIST_NS + 'rest') {
2869
- if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
2965
+ if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2870
2966
  const xs = __listElemsForBuiltin(g.s, facts);
2871
2967
  if (!xs || !xs.length) return [];
2872
2968
  const rest = new ListTerm(xs.slice(1));
@@ -2874,7 +2970,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2874
2970
  return s2 !== null ? [s2] : [];
2875
2971
  }
2876
2972
  if (pv === RDF_NS + 'rest') {
2877
- return evalListRestLikeBuiltin(g.s, g.o, subst);
2973
+ return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2878
2974
  }
2879
2975
 
2880
2976
  // list:iterate
@@ -7918,7 +8014,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
7918
8014
  const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
7919
8015
  const shouldTreatAsBuiltin =
7920
8016
  isBuiltinPred(goal0.p) &&
7921
- !(isRdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
8017
+ !(
8018
+ isRdfFirstOrRest &&
8019
+ !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm || goal0.s instanceof Var)
8020
+ );
7922
8021
 
7923
8022
  if (shouldTreatAsBuiltin) {
7924
8023
  const remaining = max - results.length;
@@ -11703,14 +11802,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
11703
11802
  const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
11704
11803
  const RDF_JSON_DT = RDF_NS + 'JSON';
11705
11804
 
11805
+ function parseUriReferenceForResolution(uri) {
11806
+ // RFC 3986 Appendix B-style component parser, with the scheme tightened to
11807
+ // the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
11808
+ // empty query is defined, while no `?` means undefined.
11809
+ const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
11810
+ if (!m) return null;
11811
+ return {
11812
+ scheme: m[2] !== undefined ? m[2] : undefined,
11813
+ authority: m[4] !== undefined ? m[4] : undefined,
11814
+ path: m[5] || '',
11815
+ query: m[6] !== undefined ? (m[7] || '') : undefined,
11816
+ fragment: m[8] !== undefined ? (m[9] || '') : undefined,
11817
+ };
11818
+ }
11819
+
11820
+ function recomposeUriReference(parts) {
11821
+ let out = '';
11822
+ if (parts.scheme !== undefined) out += `${parts.scheme}:`;
11823
+ if (parts.authority !== undefined) out += `//${parts.authority}`;
11824
+ out += parts.path || '';
11825
+ if (parts.query !== undefined) out += `?${parts.query}`;
11826
+ if (parts.fragment !== undefined) out += `#${parts.fragment}`;
11827
+ return out;
11828
+ }
11829
+
11830
+ function removeLastPathSegment(path) {
11831
+ if (!path) return '';
11832
+ const i = path.lastIndexOf('/');
11833
+ if (i < 0) return '';
11834
+ if (i === 0) return '';
11835
+ return path.slice(0, i);
11836
+ }
11837
+
11838
+ function removeDotSegments(path) {
11839
+ // RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
11840
+ // Eyeling preserves IRI spelling (for example, it does not add a trailing
11841
+ // slash to `http://example.org`) while still normalizing `.` and `..` path
11842
+ // segments as required by section 5.2.2.
11843
+ let input = String(path || '');
11844
+ let output = '';
11845
+
11846
+ while (input.length > 0) {
11847
+ if (input.startsWith('../')) {
11848
+ input = input.slice(3);
11849
+ } else if (input.startsWith('./')) {
11850
+ input = input.slice(2);
11851
+ } else if (input.startsWith('/./')) {
11852
+ input = `/${input.slice(3)}`;
11853
+ } else if (input === '/.') {
11854
+ input = '/';
11855
+ } else if (input.startsWith('/../')) {
11856
+ input = `/${input.slice(4)}`;
11857
+ output = removeLastPathSegment(output);
11858
+ } else if (input === '/..') {
11859
+ input = '/';
11860
+ output = removeLastPathSegment(output);
11861
+ } else if (input === '.' || input === '..') {
11862
+ input = '';
11863
+ } else {
11864
+ let segmentEnd;
11865
+ if (input[0] === '/') {
11866
+ segmentEnd = input.indexOf('/', 1);
11867
+ } else {
11868
+ segmentEnd = input.indexOf('/');
11869
+ }
11870
+
11871
+ if (segmentEnd < 0) {
11872
+ output += input;
11873
+ input = '';
11874
+ } else {
11875
+ output += input.slice(0, segmentEnd);
11876
+ input = input.slice(segmentEnd);
11877
+ }
11878
+ }
11879
+ }
11880
+
11881
+ return output;
11882
+ }
11883
+
11884
+ function mergePaths(base, refPath) {
11885
+ if (base.authority !== undefined && base.path === '') {
11886
+ return `/${refPath}`;
11887
+ }
11888
+ const i = base.path.lastIndexOf('/');
11889
+ if (i < 0) return refPath;
11890
+ return `${base.path.slice(0, i + 1)}${refPath}`;
11891
+ }
11892
+
11706
11893
  function resolveIriRef(ref, base) {
11707
- if (!base) return ref;
11708
- if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(ref)) return ref; // already absolute
11709
- try {
11710
- return new URL(ref, base).toString();
11711
- } catch {
11712
- return ref;
11894
+ const r = parseUriReferenceForResolution(ref);
11895
+ if (!r) return ref;
11896
+
11897
+ const baseParts = base ? parseUriReferenceForResolution(base) : null;
11898
+
11899
+ // Absolute references do not need a base, but RFC 3986 section 5.2.2 still
11900
+ // applies remove_dot_segments(R.path) when R.scheme is defined.
11901
+ if (r.scheme !== undefined) {
11902
+ return recomposeUriReference({
11903
+ scheme: r.scheme,
11904
+ authority: r.authority,
11905
+ path: removeDotSegments(r.path),
11906
+ query: r.query,
11907
+ fragment: r.fragment,
11908
+ });
11713
11909
  }
11910
+
11911
+ // Without a usable base, preserve relative references as written.
11912
+ if (!baseParts || baseParts.scheme === undefined) return ref;
11913
+
11914
+ const t = {
11915
+ scheme: baseParts.scheme,
11916
+ authority: undefined,
11917
+ path: '',
11918
+ query: undefined,
11919
+ fragment: r.fragment,
11920
+ };
11921
+
11922
+ if (r.authority !== undefined) {
11923
+ t.authority = r.authority;
11924
+ t.path = removeDotSegments(r.path);
11925
+ t.query = r.query;
11926
+ } else if (r.path === '') {
11927
+ t.authority = baseParts.authority;
11928
+ t.path = baseParts.path;
11929
+ t.query = r.query !== undefined ? r.query : baseParts.query;
11930
+ } else {
11931
+ t.authority = baseParts.authority;
11932
+ if (r.path.startsWith('/')) {
11933
+ t.path = removeDotSegments(r.path);
11934
+ } else {
11935
+ t.path = removeDotSegments(mergePaths(baseParts, r.path));
11936
+ }
11937
+ t.query = r.query;
11938
+ }
11939
+
11940
+ return recomposeUriReference(t);
11714
11941
  }
11715
11942
 
11716
11943
  // -----------------------------------------------------------------------------
package/lib/builtins.js CHANGED
@@ -1680,15 +1680,85 @@ function __listElemsForBuiltin(listLike, facts) {
1680
1680
  return null;
1681
1681
  }
1682
1682
 
1683
- function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
1684
- if (!(sTerm instanceof ListTerm)) return [];
1685
- if (!sTerm.elems.length) return [];
1686
- const first = sTerm.elems[0];
1687
- const s2 = unifyTerm(oTerm, first, subst);
1688
- return s2 !== null ? [s2] : [];
1683
+ function __pushBuiltinDeltaLimited(out, delta, maxResults) {
1684
+ if (delta === null) return false;
1685
+ out.push(delta);
1686
+ return typeof maxResults === 'number' && maxResults > 0 && out.length >= maxResults;
1687
+ }
1688
+
1689
+ function __collectListLikeTermsFromTerm(t, out, seen) {
1690
+ if (t instanceof ListTerm || t instanceof OpenListTerm) {
1691
+ const k = termFastKey ? termFastKey(t) : null;
1692
+ if (k === null || !seen.has(k)) {
1693
+ if (k !== null) seen.add(k);
1694
+ out.push(t);
1695
+ }
1696
+ if (t instanceof ListTerm) {
1697
+ for (const e of t.elems) __collectListLikeTermsFromTerm(e, out, seen);
1698
+ } else {
1699
+ for (const e of t.prefix) __collectListLikeTermsFromTerm(e, out, seen);
1700
+ }
1701
+ return;
1702
+ }
1703
+ if (t instanceof GraphTerm) {
1704
+ for (const tr of t.triples) {
1705
+ __collectListLikeTermsFromTerm(tr.s, out, seen);
1706
+ __collectListLikeTermsFromTerm(tr.p, out, seen);
1707
+ __collectListLikeTermsFromTerm(tr.o, out, seen);
1708
+ }
1709
+ }
1710
+ }
1711
+
1712
+ function __collectListLikeTermsFromFacts(facts) {
1713
+ const out = [];
1714
+ const seen = new Set();
1715
+ for (const tr of facts) {
1716
+ __collectListLikeTermsFromTerm(tr.s, out, seen);
1717
+ __collectListLikeTermsFromTerm(tr.o, out, seen);
1718
+ }
1719
+ return out;
1720
+ }
1721
+
1722
+ function evalListFirstLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
1723
+ if (sTerm instanceof ListTerm) {
1724
+ if (!sTerm.elems.length) return [];
1725
+ const first = sTerm.elems[0];
1726
+ const s2 = unifyTerm(oTerm, first, subst);
1727
+ return s2 !== null ? [s2] : [];
1728
+ }
1729
+
1730
+ // For a variable subject, enumerate already-existing collection terms.
1731
+ // This lets a rule body such as `_:x rdf:first 1; rdf:rest rdf:nil`
1732
+ // bind `_:x` to an N3 collection literal `(1)` used elsewhere as a term.
1733
+ // Also include ordinary rdf:first facts so the builtin path does not hide
1734
+ // RDF-serialized lists when the subject starts unbound.
1735
+ if (sTerm instanceof Var) {
1736
+ const out = [];
1737
+
1738
+ for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
1739
+ if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
1740
+ let s2 = unifyTerm(sTerm, listTerm, subst);
1741
+ if (s2 === null) continue;
1742
+ s2 = unifyTerm(oTerm, listTerm.elems[0], s2);
1743
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1744
+ }
1745
+
1746
+ const RDF_FIRST = RDF_NS + 'first';
1747
+ for (const tr of facts) {
1748
+ if (!(tr.p instanceof Iri) || tr.p.value !== RDF_FIRST) continue;
1749
+ let s2 = unifyTerm(sTerm, tr.s, subst);
1750
+ if (s2 === null) continue;
1751
+ s2 = unifyTerm(oTerm, tr.o, s2);
1752
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1753
+ }
1754
+
1755
+ return out;
1756
+ }
1757
+
1758
+ return [];
1689
1759
  }
1690
1760
 
1691
- function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
1761
+ function evalListRestLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
1692
1762
  // Closed list: (a b c) -> (b c)
1693
1763
  if (sTerm instanceof ListTerm) {
1694
1764
  if (!sTerm.elems.length) return [];
@@ -1709,6 +1779,32 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
1709
1779
  return s2 !== null ? [s2] : [];
1710
1780
  }
1711
1781
 
1782
+ // See evalListFirstLikeBuiltin(): if the collection subject is still a
1783
+ // variable, enumerate known list literals and ordinary rdf:rest facts.
1784
+ if (sTerm instanceof Var) {
1785
+ const out = [];
1786
+
1787
+ for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
1788
+ if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
1789
+ let s2 = unifyTerm(sTerm, listTerm, subst);
1790
+ if (s2 === null) continue;
1791
+ const rest = new ListTerm(listTerm.elems.slice(1));
1792
+ s2 = unifyTerm(oTerm, rest, s2);
1793
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1794
+ }
1795
+
1796
+ const RDF_REST = RDF_NS + 'rest';
1797
+ for (const tr of facts) {
1798
+ if (!(tr.p instanceof Iri) || tr.p.value !== RDF_REST) continue;
1799
+ let s2 = unifyTerm(sTerm, tr.s, subst);
1800
+ if (s2 === null) continue;
1801
+ s2 = unifyTerm(oTerm, tr.o, s2);
1802
+ if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
1803
+ }
1804
+
1805
+ return out;
1806
+ }
1807
+
1712
1808
  return [];
1713
1809
  }
1714
1810
 
@@ -2848,14 +2944,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2848
2944
  return s2 !== null ? [s2] : [];
2849
2945
  }
2850
2946
  if (pv === RDF_NS + 'first') {
2851
- return evalListFirstLikeBuiltin(g.s, g.o, subst);
2947
+ return evalListFirstLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2852
2948
  }
2853
2949
 
2854
2950
  // list:rest and rdf:rest
2855
2951
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
2856
2952
  // Schema: $s+ list:rest $o-
2857
2953
  if (pv === LIST_NS + 'rest') {
2858
- if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
2954
+ if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2859
2955
  const xs = __listElemsForBuiltin(g.s, facts);
2860
2956
  if (!xs || !xs.length) return [];
2861
2957
  const rest = new ListTerm(xs.slice(1));
@@ -2863,7 +2959,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
2863
2959
  return s2 !== null ? [s2] : [];
2864
2960
  }
2865
2961
  if (pv === RDF_NS + 'rest') {
2866
- return evalListRestLikeBuiltin(g.s, g.o, subst);
2962
+ return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
2867
2963
  }
2868
2964
 
2869
2965
  // list:iterate
package/lib/engine.js CHANGED
@@ -2600,7 +2600,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
2600
2600
  const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
2601
2601
  const shouldTreatAsBuiltin =
2602
2602
  isBuiltinPred(goal0.p) &&
2603
- !(isRdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
2603
+ !(
2604
+ isRdfFirstOrRest &&
2605
+ !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm || goal0.s instanceof Var)
2606
+ );
2604
2607
 
2605
2608
  if (shouldTreatAsBuiltin) {
2606
2609
  const remaining = max - results.length;
package/lib/prelude.js CHANGED
@@ -24,14 +24,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
24
24
  const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
25
25
  const RDF_JSON_DT = RDF_NS + 'JSON';
26
26
 
27
+ function parseUriReferenceForResolution(uri) {
28
+ // RFC 3986 Appendix B-style component parser, with the scheme tightened to
29
+ // the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
30
+ // empty query is defined, while no `?` means undefined.
31
+ const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
32
+ if (!m) return null;
33
+ return {
34
+ scheme: m[2] !== undefined ? m[2] : undefined,
35
+ authority: m[4] !== undefined ? m[4] : undefined,
36
+ path: m[5] || '',
37
+ query: m[6] !== undefined ? (m[7] || '') : undefined,
38
+ fragment: m[8] !== undefined ? (m[9] || '') : undefined,
39
+ };
40
+ }
41
+
42
+ function recomposeUriReference(parts) {
43
+ let out = '';
44
+ if (parts.scheme !== undefined) out += `${parts.scheme}:`;
45
+ if (parts.authority !== undefined) out += `//${parts.authority}`;
46
+ out += parts.path || '';
47
+ if (parts.query !== undefined) out += `?${parts.query}`;
48
+ if (parts.fragment !== undefined) out += `#${parts.fragment}`;
49
+ return out;
50
+ }
51
+
52
+ function removeLastPathSegment(path) {
53
+ if (!path) return '';
54
+ const i = path.lastIndexOf('/');
55
+ if (i < 0) return '';
56
+ if (i === 0) return '';
57
+ return path.slice(0, i);
58
+ }
59
+
60
+ function removeDotSegments(path) {
61
+ // RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
62
+ // Eyeling preserves IRI spelling (for example, it does not add a trailing
63
+ // slash to `http://example.org`) while still normalizing `.` and `..` path
64
+ // segments as required by section 5.2.2.
65
+ let input = String(path || '');
66
+ let output = '';
67
+
68
+ while (input.length > 0) {
69
+ if (input.startsWith('../')) {
70
+ input = input.slice(3);
71
+ } else if (input.startsWith('./')) {
72
+ input = input.slice(2);
73
+ } else if (input.startsWith('/./')) {
74
+ input = `/${input.slice(3)}`;
75
+ } else if (input === '/.') {
76
+ input = '/';
77
+ } else if (input.startsWith('/../')) {
78
+ input = `/${input.slice(4)}`;
79
+ output = removeLastPathSegment(output);
80
+ } else if (input === '/..') {
81
+ input = '/';
82
+ output = removeLastPathSegment(output);
83
+ } else if (input === '.' || input === '..') {
84
+ input = '';
85
+ } else {
86
+ let segmentEnd;
87
+ if (input[0] === '/') {
88
+ segmentEnd = input.indexOf('/', 1);
89
+ } else {
90
+ segmentEnd = input.indexOf('/');
91
+ }
92
+
93
+ if (segmentEnd < 0) {
94
+ output += input;
95
+ input = '';
96
+ } else {
97
+ output += input.slice(0, segmentEnd);
98
+ input = input.slice(segmentEnd);
99
+ }
100
+ }
101
+ }
102
+
103
+ return output;
104
+ }
105
+
106
+ function mergePaths(base, refPath) {
107
+ if (base.authority !== undefined && base.path === '') {
108
+ return `/${refPath}`;
109
+ }
110
+ const i = base.path.lastIndexOf('/');
111
+ if (i < 0) return refPath;
112
+ return `${base.path.slice(0, i + 1)}${refPath}`;
113
+ }
114
+
27
115
  function resolveIriRef(ref, base) {
28
- if (!base) return ref;
29
- if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(ref)) return ref; // already absolute
30
- try {
31
- return new URL(ref, base).toString();
32
- } catch {
33
- return ref;
116
+ const r = parseUriReferenceForResolution(ref);
117
+ if (!r) return ref;
118
+
119
+ const baseParts = base ? parseUriReferenceForResolution(base) : null;
120
+
121
+ // Absolute references do not need a base, but RFC 3986 section 5.2.2 still
122
+ // applies remove_dot_segments(R.path) when R.scheme is defined.
123
+ if (r.scheme !== undefined) {
124
+ return recomposeUriReference({
125
+ scheme: r.scheme,
126
+ authority: r.authority,
127
+ path: removeDotSegments(r.path),
128
+ query: r.query,
129
+ fragment: r.fragment,
130
+ });
34
131
  }
132
+
133
+ // Without a usable base, preserve relative references as written.
134
+ if (!baseParts || baseParts.scheme === undefined) return ref;
135
+
136
+ const t = {
137
+ scheme: baseParts.scheme,
138
+ authority: undefined,
139
+ path: '',
140
+ query: undefined,
141
+ fragment: r.fragment,
142
+ };
143
+
144
+ if (r.authority !== undefined) {
145
+ t.authority = r.authority;
146
+ t.path = removeDotSegments(r.path);
147
+ t.query = r.query;
148
+ } else if (r.path === '') {
149
+ t.authority = baseParts.authority;
150
+ t.path = baseParts.path;
151
+ t.query = r.query !== undefined ? r.query : baseParts.query;
152
+ } else {
153
+ t.authority = baseParts.authority;
154
+ if (r.path.startsWith('/')) {
155
+ t.path = removeDotSegments(r.path);
156
+ } else {
157
+ t.path = removeDotSegments(mergePaths(baseParts, r.path));
158
+ }
159
+ t.query = r.query;
160
+ }
161
+
162
+ return recomposeUriReference(t);
35
163
  }
36
164
 
37
165
  // -----------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.24",
3
+ "version": "1.24.25",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -1245,6 +1245,35 @@ ${U('o1')} ${U('path')} (${U('c')} ${U('d')}).
1245
1245
  ],
1246
1246
  },
1247
1247
 
1248
+ {
1249
+ name: '49c rdf:first/rest bind an unbound rule blank to an N3 list literal',
1250
+ opt: { proofComments: false },
1251
+ input: `@prefix : <http://example.org/> .
1252
+
1253
+ (1) <http://a.example/p> <http://a.example/o> .
1254
+
1255
+ {
1256
+ _:el1 rdf:first 1 .
1257
+ _:el1 rdf:rest rdf:nil .
1258
+ _:el1 <http://a.example/p> <http://a.example/o> .
1259
+ }
1260
+ =>
1261
+ {
1262
+ :result :has :success-literal-25 .
1263
+ }.
1264
+
1265
+ { :result :has :success-literal-25 . }
1266
+ =>
1267
+ {
1268
+ :test :is true .
1269
+ }.
1270
+ `,
1271
+ expect: [
1272
+ /:result\s+:has\s+:success-literal-25\s*\./,
1273
+ /:test\s+:is\s+true\s*\./,
1274
+ ],
1275
+ },
1276
+
1248
1277
  {
1249
1278
  name: '50 rdf collection materialization: rdf:first/rdf:rest triples become list terms',
1250
1279
  opt: { proofComments: false },