eyeling 1.10.7 → 1.10.9

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.
@@ -0,0 +1,65 @@
1
+ # ===========================
2
+ # get UUID
3
+ # Example from Wout Slabbinck
4
+ # ===========================
5
+
6
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
7
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
8
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
9
+ @prefix crypto: <http://www.w3.org/2000/10/swap/crypto#> .
10
+ @prefix odrl: <http://www.w3.org/ns/odrl/2/>.
11
+ @prefix : <http://example.org/ns#> .
12
+
13
+ # uuid backward rule (component)
14
+ {
15
+ ?input :getUUID ?urnUuid
16
+ }
17
+ <=
18
+ {
19
+ # 1. Convert input to a stable string
20
+ ?input :listToString ?inputString .
21
+
22
+ # 2. Generate a stable hash (works identically in 2026 eyeJS and Eyeling)
23
+ ?inputString crypto:sha ?hash .
24
+
25
+ # 3. Format the hash into a UUID-compliant structure (8-4-4-4-12)
26
+ ( ?hash "^(.{8}).*$" ) string:scrape ?p1 .
27
+ ( ?hash "^.{8}(.{4}).*$" ) string:scrape ?p2 .
28
+ ( ?hash "^.{12}(.{4}).*$" ) string:scrape ?p3 .
29
+ ( ?hash "^.{16}(.{4}).*$" ) string:scrape ?p4 .
30
+ ( ?hash "^.{20}(.{12}).*$" ) string:scrape ?p5 .
31
+ ( "urn:uuid:" ?p1 "-" ?p2 "-" ?p3 "-" ?p4 "-" ?p5 ) string:concatenation ?urnUuidString .
32
+
33
+ # 4. Cast the final string to a URI
34
+ ?urnUuid log:uri ?urnUuidString .
35
+ } .
36
+
37
+ # 1. Base Case: An empty list results in an empty string
38
+ { () :listToString "" } <= { } .
39
+
40
+ # 2. Recursive Case: Process list using rdf:first and rdf:rest
41
+ { ?list :listToString ?result } <= {
42
+ ?list rdf:first ?head .
43
+ ?list rdf:rest ?tail .
44
+
45
+ # Convert the current URI to a string
46
+ ?head log:uri ?headStr .
47
+
48
+ # Recursively convert the tail of the list
49
+ ?tail :listToString ?tailStr .
50
+
51
+ # Combine head and tail strings
52
+ ( ?headStr ?tailStr ) string:concatenation ?result .
53
+ } .
54
+
55
+ <test> a odrl:Policy .
56
+ <lol> a odrl:Policy .
57
+
58
+ {
59
+ ?policy a odrl:Policy .
60
+ ( ?policy ) :getUUID ?urnUuid .
61
+ }
62
+ =>
63
+ {
64
+ ?urnUuid <a> <b> .
65
+ } .
@@ -0,0 +1,2 @@
1
+ <urn:uuid:a94a8fe5-ccb1-9ba6-1c4c-0873d391e987> <a> <b> .
2
+ <urn:uuid:40392603-3d00-1b52-79df-37cbbe5287b7> <a> <b> .
package/eyeling.js CHANGED
@@ -171,8 +171,8 @@ function main() {
171
171
  process.exit(0);
172
172
  }
173
173
 
174
- // Build internal ListTerm values from rdf:first/rdf:rest (+ rdf:nil)
175
- engine.materializeRdfLists(triples, frules, brules);
174
+ // NOTE: Do not rewrite rdf:first/rdf:rest RDF list nodes into list terms.
175
+ // list:* builtins interpret RDF list structures directly when needed.
176
176
 
177
177
  const facts = triples.filter((tr) => engine.isGroundTriple(tr));
178
178
 
@@ -1195,6 +1195,7 @@ function alphaEqTriple(a, b) {
1195
1195
 
1196
1196
  function termFastKey(t) {
1197
1197
  if (t instanceof Iri) return 'I:' + t.value;
1198
+ if (t instanceof Blank) return 'B:' + t.label;
1198
1199
  if (t instanceof Literal) return 'L:' + normalizeLiteralForFastKey(t.value);
1199
1200
  return null;
1200
1201
  }
@@ -2731,6 +2732,118 @@ function listAppendSplit(parts, resElems, subst) {
2731
2732
  return out;
2732
2733
  }
2733
2734
 
2735
+
2736
+ // ---------------------------------------------------------------------------
2737
+ // RDF-list support for list:* builtins
2738
+ // ---------------------------------------------------------------------------
2739
+
2740
+ function __rdfListObjectsForSP(facts, predIri, subj) {
2741
+ ensureFactIndexes(facts);
2742
+ const sk = termFastKey(subj);
2743
+ if (sk !== null) {
2744
+ const ps = facts.__byPS.get(predIri);
2745
+ if (ps) {
2746
+ const bucket = ps.get(sk);
2747
+ if (bucket && bucket.length) return bucket.map((tr) => tr.o);
2748
+ }
2749
+ }
2750
+
2751
+ // Fallback scan (covers non-indexable terms)
2752
+ const pb = facts.__byPred.get(predIri) || [];
2753
+ const out = [];
2754
+ for (const tr of pb) {
2755
+ if (termsEqual(tr.s, subj)) out.push(tr.o);
2756
+ }
2757
+ return out;
2758
+ }
2759
+
2760
+ function __rdfListElemsFromNode(head, facts) {
2761
+ if (!(head instanceof Iri || head instanceof Blank)) return null;
2762
+
2763
+ // Cache per fact-set (important in forward chaining)
2764
+ if (!Object.prototype.hasOwnProperty.call(facts, '__rdfListCache')) {
2765
+ Object.defineProperty(facts, '__rdfListCache', {
2766
+ value: new Map(),
2767
+ enumerable: false,
2768
+ writable: true,
2769
+ configurable: true,
2770
+ });
2771
+ }
2772
+
2773
+ const key = termFastKey(head);
2774
+ if (key === null) return null;
2775
+ const cache = facts.__rdfListCache;
2776
+ if (cache.has(key)) return cache.get(key);
2777
+
2778
+ const RDF_FIRST = RDF_NS + 'first';
2779
+ const RDF_REST = RDF_NS + 'rest';
2780
+ const RDF_NIL = RDF_NS + 'nil';
2781
+
2782
+ const elems = [];
2783
+ const seen = new Set();
2784
+ let cur = head;
2785
+
2786
+ while (true) {
2787
+ if (cur instanceof Iri && cur.value === RDF_NIL) {
2788
+ cache.set(key, elems);
2789
+ return elems;
2790
+ }
2791
+
2792
+ if (!(cur instanceof Iri || cur instanceof Blank)) {
2793
+ cache.set(key, null);
2794
+ return null;
2795
+ }
2796
+
2797
+ const ck = termFastKey(cur);
2798
+ if (ck === null) {
2799
+ cache.set(key, null);
2800
+ return null;
2801
+ }
2802
+ if (seen.has(ck)) {
2803
+ cache.set(key, null);
2804
+ return null; // cycle
2805
+ }
2806
+ seen.add(ck);
2807
+
2808
+ const firsts = __rdfListObjectsForSP(facts, RDF_FIRST, cur);
2809
+ const rests = __rdfListObjectsForSP(facts, RDF_REST, cur);
2810
+
2811
+ if (firsts.length !== 1 || rests.length !== 1) {
2812
+ cache.set(key, null);
2813
+ return null;
2814
+ }
2815
+
2816
+ elems.push(firsts[0]);
2817
+ const rest = rests[0];
2818
+
2819
+ if (rest instanceof Iri && rest.value === RDF_NIL) {
2820
+ cache.set(key, elems);
2821
+ return elems;
2822
+ }
2823
+
2824
+ // Mixed tail: rdf:rest can be an N3 list literal (e.g., (:b))
2825
+ if (rest instanceof ListTerm) {
2826
+ elems.push(...rest.elems);
2827
+ cache.set(key, elems);
2828
+ return elems;
2829
+ }
2830
+ if (rest instanceof OpenListTerm) {
2831
+ elems.push(...rest.prefix);
2832
+ elems.push(new Var(rest.tailVar));
2833
+ cache.set(key, elems);
2834
+ return elems;
2835
+ }
2836
+
2837
+ cur = rest;
2838
+ }
2839
+ }
2840
+
2841
+ function __listElemsForBuiltin(listLike, facts) {
2842
+ if (listLike instanceof ListTerm) return listLike.elems;
2843
+ if (listLike instanceof Iri || listLike instanceof Blank) return __rdfListElemsFromNode(listLike, facts);
2844
+ return null;
2845
+ }
2846
+
2734
2847
  function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
2735
2848
  if (!(sTerm instanceof ListTerm)) return [];
2736
2849
  if (!sTerm.elems.length) return [];
@@ -3717,14 +3830,28 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3717
3830
  // list:first and rdf:first
3718
3831
  // true iff $s is a list and $o is the first member of that list.
3719
3832
  // Schema: $s+ list:first $o-
3720
- if (pv === LIST_NS + 'first' || pv === RDF_NS + 'first') {
3833
+ if (pv === LIST_NS + 'first') {
3834
+ const xs = __listElemsForBuiltin(g.s, facts);
3835
+ if (!xs || !xs.length) return [];
3836
+ const s2 = unifyTerm(g.o, xs[0], subst);
3837
+ return s2 !== null ? [s2] : [];
3838
+ }
3839
+ if (pv === RDF_NS + 'first') {
3721
3840
  return evalListFirstLikeBuiltin(g.s, g.o, subst);
3722
3841
  }
3723
3842
 
3724
3843
  // list:rest and rdf:rest
3725
3844
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
3726
3845
  // Schema: $s+ list:rest $o-
3727
- if (pv === LIST_NS + 'rest' || pv === RDF_NS + 'rest') {
3846
+ if (pv === LIST_NS + 'rest') {
3847
+ if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
3848
+ const xs = __listElemsForBuiltin(g.s, facts);
3849
+ if (!xs || !xs.length) return [];
3850
+ const rest = new ListTerm(xs.slice(1));
3851
+ const s2 = unifyTerm(g.o, rest, subst);
3852
+ return s2 !== null ? [s2] : [];
3853
+ }
3854
+ if (pv === RDF_NS + 'rest') {
3728
3855
  return evalListRestLikeBuiltin(g.s, g.o, subst);
3729
3856
  }
3730
3857
 
@@ -3733,8 +3860,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3733
3860
  // For a list subject $s, generate solutions by unifying $o with (index value).
3734
3861
  // This allows $o to be a variable (e.g., ?Y) or a pattern (e.g., (?i "Dewey")).
3735
3862
  if (pv === LIST_NS + 'iterate') {
3736
- if (!(g.s instanceof ListTerm)) return [];
3737
- const xs = g.s.elems;
3863
+ const xs = __listElemsForBuiltin(g.s, facts);
3864
+ if (!xs) return [];
3738
3865
  const outs = [];
3739
3866
 
3740
3867
  for (let i = 0; i < xs.length; i++) {
@@ -3774,9 +3901,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3774
3901
  // true iff $s is a list and $o is the last member of that list.
3775
3902
  // Schema: $s+ list:last $o-
3776
3903
  if (pv === LIST_NS + 'last') {
3777
- if (!(g.s instanceof ListTerm)) return [];
3778
- const xs = g.s.elems;
3779
- if (!xs.length) return [];
3904
+ const xs = __listElemsForBuiltin(g.s, facts);
3905
+ if (!xs || !xs.length) return [];
3780
3906
  const last = xs[xs.length - 1];
3781
3907
  const s2 = unifyTerm(g.o, last, subst);
3782
3908
  return s2 !== null ? [s2] : [];
@@ -3787,10 +3913,10 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3787
3913
  // Schema: ( $s.1+ $s.2?[*] )+ list:memberAt $o?[*]
3788
3914
  if (pv === LIST_NS + 'memberAt') {
3789
3915
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3790
- const [listTerm, indexTerm] = g.s.elems;
3791
- if (!(listTerm instanceof ListTerm)) return [];
3916
+ const [listRef, indexTerm] = g.s.elems;
3792
3917
 
3793
- const xs = listTerm.elems;
3918
+ const xs = __listElemsForBuiltin(listRef, facts);
3919
+ if (!xs) return [];
3794
3920
  const outs = [];
3795
3921
 
3796
3922
  for (let i = 0; i < xs.length; i++) {
@@ -3847,9 +3973,10 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3847
3973
 
3848
3974
  // list:member
3849
3975
  if (pv === LIST_NS + 'member') {
3850
- if (!(g.s instanceof ListTerm)) return [];
3976
+ const xs = __listElemsForBuiltin(g.s, facts);
3977
+ if (!xs) return [];
3851
3978
  const outs = [];
3852
- for (const x of g.s.elems) {
3979
+ for (const x of xs) {
3853
3980
  const s2 = unifyTerm(g.o, x, subst);
3854
3981
  if (s2 !== null) outs.push(s2);
3855
3982
  }
@@ -3869,8 +3996,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3869
3996
 
3870
3997
  // list:length (strict: do not accept integer<->decimal matches for a ground object)
3871
3998
  if (pv === LIST_NS + 'length') {
3872
- if (!(g.s instanceof ListTerm)) return [];
3873
- const nTerm = internLiteral(String(g.s.elems.length));
3999
+ const xs = __listElemsForBuiltin(g.s, facts);
4000
+ if (!xs) return [];
4001
+ const nTerm = internLiteral(String(xs.length));
3874
4002
 
3875
4003
  const o2 = applySubstTerm(g.o, subst);
3876
4004
  if (isGroundTerm(o2)) {
@@ -3883,8 +4011,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3883
4011
 
3884
4012
  // list:notMember
3885
4013
  if (pv === LIST_NS + 'notMember') {
3886
- if (!(g.s instanceof ListTerm)) return [];
3887
- for (const el of g.s.elems) {
4014
+ const xs = __listElemsForBuiltin(g.s, facts);
4015
+ if (!xs) return [];
4016
+ for (const el of xs) {
3888
4017
  if (unifyTerm(g.o, el, subst) !== null) return [];
3889
4018
  }
3890
4019
  return [{ ...subst }];
@@ -3892,12 +4021,23 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3892
4021
 
3893
4022
  // list:reverse
3894
4023
  if (pv === LIST_NS + 'reverse') {
4024
+ // Forward: compute o from s
3895
4025
  if (g.s instanceof ListTerm) {
3896
4026
  const rev = [...g.s.elems].reverse();
3897
4027
  const rterm = new ListTerm(rev);
3898
4028
  const s2 = unifyTerm(g.o, rterm, subst);
3899
4029
  return s2 !== null ? [s2] : [];
3900
4030
  }
4031
+
4032
+ const xs = __listElemsForBuiltin(g.s, facts);
4033
+ if (xs) {
4034
+ const rev = [...xs].reverse();
4035
+ const rterm = new ListTerm(rev);
4036
+ const s2 = unifyTerm(g.o, rterm, subst);
4037
+ return s2 !== null ? [s2] : [];
4038
+ }
4039
+
4040
+ // Reverse: compute s from o (only for explicit list terms)
3901
4041
  if (g.o instanceof ListTerm) {
3902
4042
  const rev = [...g.o.elems].reverse();
3903
4043
  const rterm = new ListTerm(rev);
@@ -5269,7 +5409,12 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
5269
5409
  const goal0 = applySubstTriple(rawGoal, state.subst);
5270
5410
 
5271
5411
  // 1) Builtins
5272
- if (isBuiltinPred(goal0.p)) {
5412
+ const __pv0 = goal0.p instanceof Iri ? goal0.p.value : null;
5413
+ const __rdfFirstOrRest = __pv0 === RDF_NS + 'first' || __pv0 === RDF_NS + 'rest';
5414
+ const __treatBuiltin =
5415
+ isBuiltinPred(goal0.p) && !(__rdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
5416
+
5417
+ if (__treatBuiltin) {
5273
5418
  const remaining = max - results.length;
5274
5419
  if (remaining <= 0) return results;
5275
5420
  const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
@@ -5932,7 +6077,8 @@ function reasonStream(n3Text, opts = {}) {
5932
6077
  // Make the parsed prefixes available to log:trace output
5933
6078
  trace.setTracePrefixes(prefixes);
5934
6079
 
5935
- materializeRdfLists(triples, frules, brules);
6080
+ // NOTE: Do not rewrite rdf:first/rdf:rest RDF list nodes into list terms.
6081
+ // list:* builtins interpret RDF list structures directly when needed.
5936
6082
 
5937
6083
  // facts becomes the saturated closure because pushFactIndexed(...) appends into it
5938
6084
  const facts = triples.filter((tr) => isGroundTriple(tr));
@@ -7459,6 +7605,21 @@ function isLogImpliedBy(p) {
7459
7605
  // PREFIX ENVIRONMENT
7460
7606
  // ===========================================================================
7461
7607
 
7608
+
7609
+ // Conservative check for whether a candidate local part can be safely serialized as a prefixed name.
7610
+ // If false, we fall back to <IRI> to guarantee syntactically valid N3/Turtle output.
7611
+ function isValidQNameLocal(local) {
7612
+ if (typeof local !== 'string' || local.length === 0) return false;
7613
+ // Disallow characters that would break PN_LOCAL unless escaped (we keep this conservative).
7614
+ if (/[#:\/\?\s]/.test(local)) return false;
7615
+ // Allow a safe ASCII subset.
7616
+ if (/[^A-Za-z0-9._-]/.test(local)) return false;
7617
+ // Avoid edge cases that typically require escaping.
7618
+ if (local.endsWith('.')) return false;
7619
+ if (/^[.-]/.test(local)) return false;
7620
+ return true;
7621
+ }
7622
+
7462
7623
  class PrefixEnv {
7463
7624
  constructor(map, baseIri) {
7464
7625
  this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
@@ -7505,6 +7666,8 @@ class PrefixEnv {
7505
7666
  if (iri.startsWith(base)) {
7506
7667
  const local = iri.slice(base.length);
7507
7668
  if (!local) continue;
7669
+ // Only emit a QName when the local part is safe to serialize without escaping.
7670
+ if (!isValidQNameLocal(local)) continue;
7508
7671
  const cand = [p, local];
7509
7672
  if (best === null || cand[1].length < best[1].length) best = cand;
7510
7673
  }
package/lib/cli.js CHANGED
@@ -159,8 +159,8 @@ function main() {
159
159
  process.exit(0);
160
160
  }
161
161
 
162
- // Build internal ListTerm values from rdf:first/rdf:rest (+ rdf:nil)
163
- engine.materializeRdfLists(triples, frules, brules);
162
+ // NOTE: Do not rewrite rdf:first/rdf:rest RDF list nodes into list terms.
163
+ // list:* builtins interpret RDF list structures directly when needed.
164
164
 
165
165
  const facts = triples.filter((tr) => engine.isGroundTriple(tr));
166
166
 
package/lib/engine.js CHANGED
@@ -539,6 +539,7 @@ function alphaEqTriple(a, b) {
539
539
 
540
540
  function termFastKey(t) {
541
541
  if (t instanceof Iri) return 'I:' + t.value;
542
+ if (t instanceof Blank) return 'B:' + t.label;
542
543
  if (t instanceof Literal) return 'L:' + normalizeLiteralForFastKey(t.value);
543
544
  return null;
544
545
  }
@@ -2075,6 +2076,118 @@ function listAppendSplit(parts, resElems, subst) {
2075
2076
  return out;
2076
2077
  }
2077
2078
 
2079
+
2080
+ // ---------------------------------------------------------------------------
2081
+ // RDF-list support for list:* builtins
2082
+ // ---------------------------------------------------------------------------
2083
+
2084
+ function __rdfListObjectsForSP(facts, predIri, subj) {
2085
+ ensureFactIndexes(facts);
2086
+ const sk = termFastKey(subj);
2087
+ if (sk !== null) {
2088
+ const ps = facts.__byPS.get(predIri);
2089
+ if (ps) {
2090
+ const bucket = ps.get(sk);
2091
+ if (bucket && bucket.length) return bucket.map((tr) => tr.o);
2092
+ }
2093
+ }
2094
+
2095
+ // Fallback scan (covers non-indexable terms)
2096
+ const pb = facts.__byPred.get(predIri) || [];
2097
+ const out = [];
2098
+ for (const tr of pb) {
2099
+ if (termsEqual(tr.s, subj)) out.push(tr.o);
2100
+ }
2101
+ return out;
2102
+ }
2103
+
2104
+ function __rdfListElemsFromNode(head, facts) {
2105
+ if (!(head instanceof Iri || head instanceof Blank)) return null;
2106
+
2107
+ // Cache per fact-set (important in forward chaining)
2108
+ if (!Object.prototype.hasOwnProperty.call(facts, '__rdfListCache')) {
2109
+ Object.defineProperty(facts, '__rdfListCache', {
2110
+ value: new Map(),
2111
+ enumerable: false,
2112
+ writable: true,
2113
+ configurable: true,
2114
+ });
2115
+ }
2116
+
2117
+ const key = termFastKey(head);
2118
+ if (key === null) return null;
2119
+ const cache = facts.__rdfListCache;
2120
+ if (cache.has(key)) return cache.get(key);
2121
+
2122
+ const RDF_FIRST = RDF_NS + 'first';
2123
+ const RDF_REST = RDF_NS + 'rest';
2124
+ const RDF_NIL = RDF_NS + 'nil';
2125
+
2126
+ const elems = [];
2127
+ const seen = new Set();
2128
+ let cur = head;
2129
+
2130
+ while (true) {
2131
+ if (cur instanceof Iri && cur.value === RDF_NIL) {
2132
+ cache.set(key, elems);
2133
+ return elems;
2134
+ }
2135
+
2136
+ if (!(cur instanceof Iri || cur instanceof Blank)) {
2137
+ cache.set(key, null);
2138
+ return null;
2139
+ }
2140
+
2141
+ const ck = termFastKey(cur);
2142
+ if (ck === null) {
2143
+ cache.set(key, null);
2144
+ return null;
2145
+ }
2146
+ if (seen.has(ck)) {
2147
+ cache.set(key, null);
2148
+ return null; // cycle
2149
+ }
2150
+ seen.add(ck);
2151
+
2152
+ const firsts = __rdfListObjectsForSP(facts, RDF_FIRST, cur);
2153
+ const rests = __rdfListObjectsForSP(facts, RDF_REST, cur);
2154
+
2155
+ if (firsts.length !== 1 || rests.length !== 1) {
2156
+ cache.set(key, null);
2157
+ return null;
2158
+ }
2159
+
2160
+ elems.push(firsts[0]);
2161
+ const rest = rests[0];
2162
+
2163
+ if (rest instanceof Iri && rest.value === RDF_NIL) {
2164
+ cache.set(key, elems);
2165
+ return elems;
2166
+ }
2167
+
2168
+ // Mixed tail: rdf:rest can be an N3 list literal (e.g., (:b))
2169
+ if (rest instanceof ListTerm) {
2170
+ elems.push(...rest.elems);
2171
+ cache.set(key, elems);
2172
+ return elems;
2173
+ }
2174
+ if (rest instanceof OpenListTerm) {
2175
+ elems.push(...rest.prefix);
2176
+ elems.push(new Var(rest.tailVar));
2177
+ cache.set(key, elems);
2178
+ return elems;
2179
+ }
2180
+
2181
+ cur = rest;
2182
+ }
2183
+ }
2184
+
2185
+ function __listElemsForBuiltin(listLike, facts) {
2186
+ if (listLike instanceof ListTerm) return listLike.elems;
2187
+ if (listLike instanceof Iri || listLike instanceof Blank) return __rdfListElemsFromNode(listLike, facts);
2188
+ return null;
2189
+ }
2190
+
2078
2191
  function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
2079
2192
  if (!(sTerm instanceof ListTerm)) return [];
2080
2193
  if (!sTerm.elems.length) return [];
@@ -3061,14 +3174,28 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3061
3174
  // list:first and rdf:first
3062
3175
  // true iff $s is a list and $o is the first member of that list.
3063
3176
  // Schema: $s+ list:first $o-
3064
- if (pv === LIST_NS + 'first' || pv === RDF_NS + 'first') {
3177
+ if (pv === LIST_NS + 'first') {
3178
+ const xs = __listElemsForBuiltin(g.s, facts);
3179
+ if (!xs || !xs.length) return [];
3180
+ const s2 = unifyTerm(g.o, xs[0], subst);
3181
+ return s2 !== null ? [s2] : [];
3182
+ }
3183
+ if (pv === RDF_NS + 'first') {
3065
3184
  return evalListFirstLikeBuiltin(g.s, g.o, subst);
3066
3185
  }
3067
3186
 
3068
3187
  // list:rest and rdf:rest
3069
3188
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
3070
3189
  // Schema: $s+ list:rest $o-
3071
- if (pv === LIST_NS + 'rest' || pv === RDF_NS + 'rest') {
3190
+ if (pv === LIST_NS + 'rest') {
3191
+ if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
3192
+ const xs = __listElemsForBuiltin(g.s, facts);
3193
+ if (!xs || !xs.length) return [];
3194
+ const rest = new ListTerm(xs.slice(1));
3195
+ const s2 = unifyTerm(g.o, rest, subst);
3196
+ return s2 !== null ? [s2] : [];
3197
+ }
3198
+ if (pv === RDF_NS + 'rest') {
3072
3199
  return evalListRestLikeBuiltin(g.s, g.o, subst);
3073
3200
  }
3074
3201
 
@@ -3077,8 +3204,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3077
3204
  // For a list subject $s, generate solutions by unifying $o with (index value).
3078
3205
  // This allows $o to be a variable (e.g., ?Y) or a pattern (e.g., (?i "Dewey")).
3079
3206
  if (pv === LIST_NS + 'iterate') {
3080
- if (!(g.s instanceof ListTerm)) return [];
3081
- const xs = g.s.elems;
3207
+ const xs = __listElemsForBuiltin(g.s, facts);
3208
+ if (!xs) return [];
3082
3209
  const outs = [];
3083
3210
 
3084
3211
  for (let i = 0; i < xs.length; i++) {
@@ -3118,9 +3245,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3118
3245
  // true iff $s is a list and $o is the last member of that list.
3119
3246
  // Schema: $s+ list:last $o-
3120
3247
  if (pv === LIST_NS + 'last') {
3121
- if (!(g.s instanceof ListTerm)) return [];
3122
- const xs = g.s.elems;
3123
- if (!xs.length) return [];
3248
+ const xs = __listElemsForBuiltin(g.s, facts);
3249
+ if (!xs || !xs.length) return [];
3124
3250
  const last = xs[xs.length - 1];
3125
3251
  const s2 = unifyTerm(g.o, last, subst);
3126
3252
  return s2 !== null ? [s2] : [];
@@ -3131,10 +3257,10 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3131
3257
  // Schema: ( $s.1+ $s.2?[*] )+ list:memberAt $o?[*]
3132
3258
  if (pv === LIST_NS + 'memberAt') {
3133
3259
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3134
- const [listTerm, indexTerm] = g.s.elems;
3135
- if (!(listTerm instanceof ListTerm)) return [];
3260
+ const [listRef, indexTerm] = g.s.elems;
3136
3261
 
3137
- const xs = listTerm.elems;
3262
+ const xs = __listElemsForBuiltin(listRef, facts);
3263
+ if (!xs) return [];
3138
3264
  const outs = [];
3139
3265
 
3140
3266
  for (let i = 0; i < xs.length; i++) {
@@ -3191,9 +3317,10 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3191
3317
 
3192
3318
  // list:member
3193
3319
  if (pv === LIST_NS + 'member') {
3194
- if (!(g.s instanceof ListTerm)) return [];
3320
+ const xs = __listElemsForBuiltin(g.s, facts);
3321
+ if (!xs) return [];
3195
3322
  const outs = [];
3196
- for (const x of g.s.elems) {
3323
+ for (const x of xs) {
3197
3324
  const s2 = unifyTerm(g.o, x, subst);
3198
3325
  if (s2 !== null) outs.push(s2);
3199
3326
  }
@@ -3213,8 +3340,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3213
3340
 
3214
3341
  // list:length (strict: do not accept integer<->decimal matches for a ground object)
3215
3342
  if (pv === LIST_NS + 'length') {
3216
- if (!(g.s instanceof ListTerm)) return [];
3217
- const nTerm = internLiteral(String(g.s.elems.length));
3343
+ const xs = __listElemsForBuiltin(g.s, facts);
3344
+ if (!xs) return [];
3345
+ const nTerm = internLiteral(String(xs.length));
3218
3346
 
3219
3347
  const o2 = applySubstTerm(g.o, subst);
3220
3348
  if (isGroundTerm(o2)) {
@@ -3227,8 +3355,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3227
3355
 
3228
3356
  // list:notMember
3229
3357
  if (pv === LIST_NS + 'notMember') {
3230
- if (!(g.s instanceof ListTerm)) return [];
3231
- for (const el of g.s.elems) {
3358
+ const xs = __listElemsForBuiltin(g.s, facts);
3359
+ if (!xs) return [];
3360
+ for (const el of xs) {
3232
3361
  if (unifyTerm(g.o, el, subst) !== null) return [];
3233
3362
  }
3234
3363
  return [{ ...subst }];
@@ -3236,12 +3365,23 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3236
3365
 
3237
3366
  // list:reverse
3238
3367
  if (pv === LIST_NS + 'reverse') {
3368
+ // Forward: compute o from s
3239
3369
  if (g.s instanceof ListTerm) {
3240
3370
  const rev = [...g.s.elems].reverse();
3241
3371
  const rterm = new ListTerm(rev);
3242
3372
  const s2 = unifyTerm(g.o, rterm, subst);
3243
3373
  return s2 !== null ? [s2] : [];
3244
3374
  }
3375
+
3376
+ const xs = __listElemsForBuiltin(g.s, facts);
3377
+ if (xs) {
3378
+ const rev = [...xs].reverse();
3379
+ const rterm = new ListTerm(rev);
3380
+ const s2 = unifyTerm(g.o, rterm, subst);
3381
+ return s2 !== null ? [s2] : [];
3382
+ }
3383
+
3384
+ // Reverse: compute s from o (only for explicit list terms)
3245
3385
  if (g.o instanceof ListTerm) {
3246
3386
  const rev = [...g.o.elems].reverse();
3247
3387
  const rterm = new ListTerm(rev);
@@ -4613,7 +4753,12 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
4613
4753
  const goal0 = applySubstTriple(rawGoal, state.subst);
4614
4754
 
4615
4755
  // 1) Builtins
4616
- if (isBuiltinPred(goal0.p)) {
4756
+ const __pv0 = goal0.p instanceof Iri ? goal0.p.value : null;
4757
+ const __rdfFirstOrRest = __pv0 === RDF_NS + 'first' || __pv0 === RDF_NS + 'rest';
4758
+ const __treatBuiltin =
4759
+ isBuiltinPred(goal0.p) && !(__rdfFirstOrRest && !(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm));
4760
+
4761
+ if (__treatBuiltin) {
4617
4762
  const remaining = max - results.length;
4618
4763
  if (remaining <= 0) return results;
4619
4764
  const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
@@ -5276,7 +5421,8 @@ function reasonStream(n3Text, opts = {}) {
5276
5421
  // Make the parsed prefixes available to log:trace output
5277
5422
  trace.setTracePrefixes(prefixes);
5278
5423
 
5279
- materializeRdfLists(triples, frules, brules);
5424
+ // NOTE: Do not rewrite rdf:first/rdf:rest RDF list nodes into list terms.
5425
+ // list:* builtins interpret RDF list structures directly when needed.
5280
5426
 
5281
5427
  // facts becomes the saturated closure because pushFactIndexed(...) appends into it
5282
5428
  const facts = triples.filter((tr) => isGroundTriple(tr));
package/lib/prelude.js CHANGED
@@ -217,6 +217,21 @@ function isLogImpliedBy(p) {
217
217
  // PREFIX ENVIRONMENT
218
218
  // ===========================================================================
219
219
 
220
+
221
+ // Conservative check for whether a candidate local part can be safely serialized as a prefixed name.
222
+ // If false, we fall back to <IRI> to guarantee syntactically valid N3/Turtle output.
223
+ function isValidQNameLocal(local) {
224
+ if (typeof local !== 'string' || local.length === 0) return false;
225
+ // Disallow characters that would break PN_LOCAL unless escaped (we keep this conservative).
226
+ if (/[#:\/\?\s]/.test(local)) return false;
227
+ // Allow a safe ASCII subset.
228
+ if (/[^A-Za-z0-9._-]/.test(local)) return false;
229
+ // Avoid edge cases that typically require escaping.
230
+ if (local.endsWith('.')) return false;
231
+ if (/^[.-]/.test(local)) return false;
232
+ return true;
233
+ }
234
+
220
235
  class PrefixEnv {
221
236
  constructor(map, baseIri) {
222
237
  this.map = map || {}; // prefix -> IRI (including "" for @prefix :)
@@ -263,6 +278,8 @@ class PrefixEnv {
263
278
  if (iri.startsWith(base)) {
264
279
  const local = iri.slice(base.length);
265
280
  if (!local) continue;
281
+ // Only emit a QName when the local part is safe to serialize without escaping.
282
+ if (!isValidQNameLocal(local)) continue;
266
283
  const cand = [p, local];
267
284
  if (best === null || cand[1].length < best[1].length) best = cand;
268
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.10.7",
3
+ "version": "1.10.9",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -866,6 +866,37 @@ ex:a p:trig ex:b.
866
866
  assert.ok(!String(out).includes('http://example.org/p'));
867
867
  },
868
868
  },
869
+ {
870
+ name: 'issue #6: RDF list nodes should not be rewritten; list:* builtins should traverse rdf:first/rest',
871
+ opt: {},
872
+ input: `@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
873
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
874
+ @prefix : <urn:example:> .
875
+
876
+ :path2 rdf:first :b; rdf:rest rdf:nil.
877
+ :path1 rdf:type :P; rdf:first :a; rdf:rest :path2.
878
+ :path1-nok rdf:type :P; rdf:first :a; rdf:rest (:b).
879
+
880
+ { ?p rdf:type :P. ?p rdf:first ?first. }
881
+ =>
882
+ { :result :query1 (?p ?first). }.
883
+
884
+ { ?p rdf:type :P. (?p ?i) list:memberAt ?m. }
885
+ =>
886
+ { :result :query2 (?p ?i ?m). }.
887
+ `,
888
+ expect: [
889
+ /:result\s+:query1\s+\(:path1\s+:a\)\s*\./,
890
+ /:result\s+:query1\s+\(:path1-nok\s+:a\)\s*\./,
891
+ /:result\s+:query2\s+\(:path1\s+0\s+:a\)\s*\./,
892
+ /:result\s+:query2\s+\(:path1\s+1\s+:b\)\s*\./,
893
+ /:result\s+:query2\s+\(:path1-nok\s+0\s+:a\)\s*\./,
894
+ /:result\s+:query2\s+\(:path1-nok\s+1\s+:b\)\s*\./,
895
+ ],
896
+ notExpect: [
897
+ /:result\s+:query1\s+\(\(:a\s+:b\)\s+:a\)/,
898
+ ],
899
+ }
869
900
  ];
870
901
 
871
902
  let passed = 0;