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.
- package/examples/get-uuid.n3 +65 -0
- package/examples/output/get-uuid.n3 +2 -0
- package/eyeling.js +183 -20
- package/lib/cli.js +2 -2
- package/lib/engine.js +164 -18
- package/lib/prelude.js +17 -0
- package/package.json +1 -1
- package/test/api.test.js +31 -0
|
@@ -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
|
+
} .
|
package/eyeling.js
CHANGED
|
@@ -171,8 +171,8 @@ function main() {
|
|
|
171
171
|
process.exit(0);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
//
|
|
175
|
-
|
|
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'
|
|
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'
|
|
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
|
-
|
|
3737
|
-
|
|
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
|
-
|
|
3778
|
-
|
|
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 [
|
|
3791
|
-
if (!(listTerm instanceof ListTerm)) return [];
|
|
3916
|
+
const [listRef, indexTerm] = g.s.elems;
|
|
3792
3917
|
|
|
3793
|
-
const xs =
|
|
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
|
-
|
|
3976
|
+
const xs = __listElemsForBuiltin(g.s, facts);
|
|
3977
|
+
if (!xs) return [];
|
|
3851
3978
|
const outs = [];
|
|
3852
|
-
for (const x of
|
|
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
|
-
|
|
3873
|
-
|
|
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
|
-
|
|
3887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
163
|
-
|
|
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'
|
|
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'
|
|
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
|
-
|
|
3081
|
-
|
|
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
|
-
|
|
3122
|
-
|
|
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 [
|
|
3135
|
-
if (!(listTerm instanceof ListTerm)) return [];
|
|
3260
|
+
const [listRef, indexTerm] = g.s.elems;
|
|
3136
3261
|
|
|
3137
|
-
const xs =
|
|
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
|
-
|
|
3320
|
+
const xs = __listElemsForBuiltin(g.s, facts);
|
|
3321
|
+
if (!xs) return [];
|
|
3195
3322
|
const outs = [];
|
|
3196
|
-
for (const x of
|
|
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
|
-
|
|
3217
|
-
|
|
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
|
-
|
|
3231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
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;
|