eyeling 1.24.24 → 1.24.26
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/README.md +2 -0
- package/dist/browser/eyeling.browser.js +258 -17
- package/examples/input/rdf-message-flow.trig +87 -59
- package/examples/input/rdf-message-microgrid.trig +89 -0
- package/examples/input/rdf-messages.trig +61 -40
- package/examples/output/rdf-message-flow.md +2 -2
- package/examples/output/rdf-message-microgrid.md +14 -0
- package/examples/output/rdf-messages.md +4 -2
- package/examples/rdf-message-flow.n3 +84 -69
- package/examples/rdf-message-microgrid.n3 +141 -0
- package/examples/rdf-messages.n3 +72 -52
- package/eyeling.js +258 -17
- package/lib/builtins.js +106 -10
- package/lib/engine.js +4 -1
- package/lib/parser.js +14 -0
- package/lib/prelude.js +134 -6
- package/package.json +1 -1
- package/test/api.test.js +137 -0
package/examples/rdf-messages.n3
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
# ============
|
|
2
2
|
# RDF Messages
|
|
3
3
|
# ============
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
4
|
+
#
|
|
5
|
+
# Run as:
|
|
6
|
+
#
|
|
7
|
+
# eyeling -r examples/rdf-messages.n3 examples/input/rdf-messages.trig
|
|
8
|
+
#
|
|
9
|
+
# Motivation
|
|
10
|
+
# ----------
|
|
11
|
+
# The RDF Messages draft defines an RDF Message as an RDF dataset interpreted
|
|
12
|
+
# atomically, an RDF Message Stream as an ordered sequence of messages, and an
|
|
13
|
+
# RDF Message Log as a replayable record of such a stream. The draft also says
|
|
14
|
+
# that messages should not be combined by default and that blank node labels are
|
|
15
|
+
# scoped to the message in which they occur.
|
|
16
|
+
#
|
|
17
|
+
# This Eyeling example demonstrates those ideas in ordinary N3/TriG rather than
|
|
18
|
+
# in a parser-level RDF Message Log with VERSION "1.2-messages" and MESSAGE
|
|
19
|
+
# delimiters. The companion TriG file therefore has two layers:
|
|
20
|
+
#
|
|
21
|
+
# 1. application-local envelope facts in the default graph, such as message
|
|
22
|
+
# order, offset, expected result, and the textual blank-node label seen in
|
|
23
|
+
# the original stream; and
|
|
24
|
+
# 2. one named graph per non-empty message payload, treated as the atomic RDF
|
|
25
|
+
# dataset/message to be inspected.
|
|
26
|
+
#
|
|
27
|
+
# The rules below validate a replay archive with three message envelopes. The
|
|
28
|
+
# middle message is an empty heartbeat. The first and third messages both record
|
|
29
|
+
# the local blank-node label "_:b0"; because this sidecar is normal TriG, the
|
|
30
|
+
# actual TriG blank nodes are unique, while the envelope records show the
|
|
31
|
+
# message-scoped label reuse that a true RDF Message Log parser would preserve
|
|
32
|
+
# by resetting blank-node scope at each MESSAGE delimiter.
|
|
11
33
|
|
|
12
34
|
@prefix : <https://eyereasoner.github.io/eyeling/examples/rdf-messages#>.
|
|
13
|
-
@prefix
|
|
35
|
+
@prefix rmsg: <https://eyereasoner.github.io/eyeling/examples/rdf-messages/vocab#>.
|
|
14
36
|
@prefix prov: <http://www.w3.org/ns/prov#>.
|
|
15
37
|
@prefix sosa: <http://www.w3.org/ns/sosa/>.
|
|
16
38
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
|
|
@@ -19,51 +41,49 @@
|
|
|
19
41
|
@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
20
42
|
@prefix string: <http://www.w3.org/2000/10/swap/string#>.
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Empty RDF Message: a legal heartbeat/keep-alive event.
|
|
25
|
-
|
|
26
|
-
|
|
27
44
|
# A message with an offset inside a log has an explicit replay boundary.
|
|
28
|
-
|
|
45
|
+
# The envelope resource is application-local; the payload graph is the dataset
|
|
46
|
+
# treated as the RDF Message.
|
|
29
47
|
{
|
|
30
|
-
?Log a
|
|
31
|
-
|
|
32
|
-
?Message a
|
|
33
|
-
|
|
48
|
+
?Log a rmsg:MessageLog;
|
|
49
|
+
rmsg:message ?Message.
|
|
50
|
+
?Message a rmsg:MessageEnvelope;
|
|
51
|
+
rmsg:offset ?Offset.
|
|
34
52
|
} => {
|
|
35
|
-
?Message
|
|
36
|
-
?Log
|
|
53
|
+
?Message rmsg:boundaryExplicit true.
|
|
54
|
+
?Log rmsg:replayContains ?Message.
|
|
37
55
|
}.
|
|
38
56
|
|
|
39
|
-
# The payload is a dataset-like
|
|
40
|
-
#
|
|
57
|
+
# The payload is a dataset-like named graph. Inspect it with log:includes instead
|
|
58
|
+
# of merging all payload triples into the global graph.
|
|
41
59
|
{
|
|
42
|
-
?Message a
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
?Message a rmsg:MessageEnvelope;
|
|
61
|
+
rmsg:expectedResult ?Result;
|
|
62
|
+
rmsg:payloadGraph ?Payload.
|
|
45
63
|
?Payload log:nameOf ?PayloadContext.
|
|
46
64
|
?PayloadContext log:includes { ?Observation sosa:hasSimpleResult ?Result. }.
|
|
47
65
|
} => {
|
|
48
|
-
?Message
|
|
66
|
+
?Message rmsg:payloadResult ?Result.
|
|
49
67
|
}.
|
|
50
68
|
|
|
51
|
-
#
|
|
69
|
+
# Empty messages are legal RDF Messages. Here the empty second envelope acts as
|
|
70
|
+
# a heartbeat/keep-alive and still occupies a boundary in the replay archive.
|
|
52
71
|
{
|
|
53
|
-
?Message a
|
|
54
|
-
|
|
72
|
+
?Message a rmsg:MessageEnvelope;
|
|
73
|
+
rmsg:payloadKind :heartbeat.
|
|
55
74
|
} => {
|
|
56
|
-
?Message
|
|
75
|
+
?Message rmsg:emptyMessageAllowed true.
|
|
57
76
|
:HeartbeatEvidence :accepted ?Message.
|
|
58
77
|
}.
|
|
59
78
|
|
|
60
|
-
# The same blank-node label may recur in
|
|
61
|
-
#
|
|
79
|
+
# The same local blank-node label may recur in different messages. In this
|
|
80
|
+
# plain-TriG sidecar the real blank nodes are unique, and the envelope metadata
|
|
81
|
+
# records the per-message label visible in the source stream.
|
|
62
82
|
{
|
|
63
|
-
?First a
|
|
64
|
-
|
|
65
|
-
?Second a
|
|
66
|
-
|
|
83
|
+
?First a rmsg:MessageEnvelope;
|
|
84
|
+
rmsg:localBlankLabel ?Label.
|
|
85
|
+
?Second a rmsg:MessageEnvelope;
|
|
86
|
+
rmsg:localBlankLabel ?Label.
|
|
67
87
|
?First log:notEqualTo ?Second.
|
|
68
88
|
} => {
|
|
69
89
|
:BlankNodeScope :reusedLabel ?Label.
|
|
@@ -71,35 +91,35 @@
|
|
|
71
91
|
}.
|
|
72
92
|
|
|
73
93
|
# Different observations from the same sensor are not a global contradiction:
|
|
74
|
-
#
|
|
94
|
+
# by default, message payloads remain separate communication contexts.
|
|
75
95
|
{
|
|
76
|
-
?First a
|
|
77
|
-
|
|
78
|
-
?Second a
|
|
79
|
-
|
|
96
|
+
?First a rmsg:MessageEnvelope;
|
|
97
|
+
rmsg:payloadResult ?FirstResult.
|
|
98
|
+
?Second a rmsg:MessageEnvelope;
|
|
99
|
+
rmsg:payloadResult ?SecondResult.
|
|
80
100
|
?FirstResult math:notEqualTo ?SecondResult.
|
|
81
101
|
?First log:notEqualTo ?Second.
|
|
82
102
|
} => {
|
|
83
103
|
:MessageContext :differentObservationsStayContextual true.
|
|
84
104
|
}.
|
|
85
105
|
|
|
86
|
-
# The Eyeling verdict is emitted only when all message-specific validations have
|
|
87
|
-
# derived from the log, message
|
|
106
|
+
# The Eyeling verdict is emitted only when all message-specific validations have
|
|
107
|
+
# been derived from the log metadata, ordered message list, payload graphs, and
|
|
108
|
+
# built-ins.
|
|
88
109
|
{
|
|
89
|
-
:temperatureLog
|
|
110
|
+
:temperatureLog rmsg:orderedMessages ?Messages.
|
|
90
111
|
?Messages list:length ?Count.
|
|
91
|
-
:m001
|
|
92
|
-
|
|
93
|
-
:m002
|
|
94
|
-
|
|
95
|
-
:m003
|
|
96
|
-
|
|
112
|
+
:m001 rmsg:boundaryExplicit true;
|
|
113
|
+
rmsg:payloadResult ?FirstResult.
|
|
114
|
+
:m002 rmsg:boundaryExplicit true;
|
|
115
|
+
rmsg:emptyMessageAllowed true.
|
|
116
|
+
:m003 rmsg:boundaryExplicit true;
|
|
117
|
+
rmsg:payloadResult ?SecondResult.
|
|
97
118
|
:BlankNodeScope :reusedLabel ?Label;
|
|
98
119
|
:isPerMessage true.
|
|
99
120
|
:MessageContext :differentObservationsStayContextual true.
|
|
100
|
-
("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message
|
|
121
|
+
("# rdf-messages\n\n## Source files\n\n- [N3 rules](../rdf-messages.n3)\n- [Input TriG](../input/rdf-messages.trig)\n\n## Answer\nRDF Message replay archive accepted: %d explicit message boundaries are preserved. Message :m002 is an empty heartbeat, and the local blank-node label %s is safely reused in separate message envelopes.\n\n## Explanation\nThe input is a single runnable example split across an N3 rule file and a TriG sidecar. The TriG file uses application-local envelope facts for stream order and replay metadata, while each non-empty named payload graph is treated as an atomic RDF Message dataset. Payloads are inspected with log:includes inside their own formulas, so the observation data stays inside the message boundary instead of being treated as one global graph. The two temperature results, %s and %s, are different observations from the same stream but are contextualized by their message boundaries.\n\nThis is intentionally not a parser-level VERSION \\\"1.2-messages\\\" / MESSAGE delimiter test. It is a reasoning example over an already-materialized sidecar representation of a message log." ?Count ?Label ?FirstResult ?SecondResult) string:format ?Block.
|
|
101
122
|
} => {
|
|
102
123
|
:rdfMessagesExample log:outputString ?Block.
|
|
103
124
|
:rdfMessagesExample :demonstrates :ExplicitBoundaries, :AtomicMessageContext, :EmptyHeartbeat, :MessageScopedBlankNodes, :ReplayableMessageLog.
|
|
104
125
|
}.
|
|
105
|
-
|
package/eyeling.js
CHANGED
|
@@ -1691,15 +1691,85 @@ function __listElemsForBuiltin(listLike, facts) {
|
|
|
1691
1691
|
return null;
|
|
1692
1692
|
}
|
|
1693
1693
|
|
|
1694
|
-
function
|
|
1695
|
-
if (
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
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;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function __collectListLikeTermsFromTerm(t, out, seen) {
|
|
1701
|
+
if (t instanceof ListTerm || t instanceof OpenListTerm) {
|
|
1702
|
+
const k = termFastKey(t);
|
|
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 [];
|
|
1700
1770
|
}
|
|
1701
1771
|
|
|
1702
|
-
function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
|
|
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
|
-
!(
|
|
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;
|
|
@@ -11399,6 +11498,20 @@ class Parser {
|
|
|
11399
11498
|
if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'a') {
|
|
11400
11499
|
this.next();
|
|
11401
11500
|
pred = internIri(RDF_NS + 'type');
|
|
11501
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'has') {
|
|
11502
|
+
// N3 syntactic sugar is also valid in predicate-object lists,
|
|
11503
|
+
// including blank node property lists: [ has :p :o ] means _:b :p :o.
|
|
11504
|
+
this.next();
|
|
11505
|
+
pred = this.parseTerm();
|
|
11506
|
+
} else if (this.peek().typ === 'Ident' && (this.peek().value || '') === 'is') {
|
|
11507
|
+
// N3 syntactic sugar: [ is :p of :s ] means :s :p _:b.
|
|
11508
|
+
this.next();
|
|
11509
|
+
pred = this.parseTerm();
|
|
11510
|
+
if (!(this.peek().typ === 'Ident' && (this.peek().value || '') === 'of')) {
|
|
11511
|
+
this.fail(`Expected 'of' after 'is <expr>', got ${this.peek().toString()}`);
|
|
11512
|
+
}
|
|
11513
|
+
this.next();
|
|
11514
|
+
invert = true;
|
|
11402
11515
|
} else if (this.peek().typ === 'OpPredInvert') {
|
|
11403
11516
|
this.next();
|
|
11404
11517
|
pred = this.parseTerm();
|
|
@@ -11703,14 +11816,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
|
|
|
11703
11816
|
const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
|
|
11704
11817
|
const RDF_JSON_DT = RDF_NS + 'JSON';
|
|
11705
11818
|
|
|
11819
|
+
function parseUriReferenceForResolution(uri) {
|
|
11820
|
+
// RFC 3986 Appendix B-style component parser, with the scheme tightened to
|
|
11821
|
+
// the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
|
|
11822
|
+
// empty query is defined, while no `?` means undefined.
|
|
11823
|
+
const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
|
|
11824
|
+
if (!m) return null;
|
|
11825
|
+
return {
|
|
11826
|
+
scheme: m[2] !== undefined ? m[2] : undefined,
|
|
11827
|
+
authority: m[4] !== undefined ? m[4] : undefined,
|
|
11828
|
+
path: m[5] || '',
|
|
11829
|
+
query: m[6] !== undefined ? (m[7] || '') : undefined,
|
|
11830
|
+
fragment: m[8] !== undefined ? (m[9] || '') : undefined,
|
|
11831
|
+
};
|
|
11832
|
+
}
|
|
11833
|
+
|
|
11834
|
+
function recomposeUriReference(parts) {
|
|
11835
|
+
let out = '';
|
|
11836
|
+
if (parts.scheme !== undefined) out += `${parts.scheme}:`;
|
|
11837
|
+
if (parts.authority !== undefined) out += `//${parts.authority}`;
|
|
11838
|
+
out += parts.path || '';
|
|
11839
|
+
if (parts.query !== undefined) out += `?${parts.query}`;
|
|
11840
|
+
if (parts.fragment !== undefined) out += `#${parts.fragment}`;
|
|
11841
|
+
return out;
|
|
11842
|
+
}
|
|
11843
|
+
|
|
11844
|
+
function removeLastPathSegment(path) {
|
|
11845
|
+
if (!path) return '';
|
|
11846
|
+
const i = path.lastIndexOf('/');
|
|
11847
|
+
if (i < 0) return '';
|
|
11848
|
+
if (i === 0) return '';
|
|
11849
|
+
return path.slice(0, i);
|
|
11850
|
+
}
|
|
11851
|
+
|
|
11852
|
+
function removeDotSegments(path) {
|
|
11853
|
+
// RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
|
|
11854
|
+
// Eyeling preserves IRI spelling (for example, it does not add a trailing
|
|
11855
|
+
// slash to `http://example.org`) while still normalizing `.` and `..` path
|
|
11856
|
+
// segments as required by section 5.2.2.
|
|
11857
|
+
let input = String(path || '');
|
|
11858
|
+
let output = '';
|
|
11859
|
+
|
|
11860
|
+
while (input.length > 0) {
|
|
11861
|
+
if (input.startsWith('../')) {
|
|
11862
|
+
input = input.slice(3);
|
|
11863
|
+
} else if (input.startsWith('./')) {
|
|
11864
|
+
input = input.slice(2);
|
|
11865
|
+
} else if (input.startsWith('/./')) {
|
|
11866
|
+
input = `/${input.slice(3)}`;
|
|
11867
|
+
} else if (input === '/.') {
|
|
11868
|
+
input = '/';
|
|
11869
|
+
} else if (input.startsWith('/../')) {
|
|
11870
|
+
input = `/${input.slice(4)}`;
|
|
11871
|
+
output = removeLastPathSegment(output);
|
|
11872
|
+
} else if (input === '/..') {
|
|
11873
|
+
input = '/';
|
|
11874
|
+
output = removeLastPathSegment(output);
|
|
11875
|
+
} else if (input === '.' || input === '..') {
|
|
11876
|
+
input = '';
|
|
11877
|
+
} else {
|
|
11878
|
+
let segmentEnd;
|
|
11879
|
+
if (input[0] === '/') {
|
|
11880
|
+
segmentEnd = input.indexOf('/', 1);
|
|
11881
|
+
} else {
|
|
11882
|
+
segmentEnd = input.indexOf('/');
|
|
11883
|
+
}
|
|
11884
|
+
|
|
11885
|
+
if (segmentEnd < 0) {
|
|
11886
|
+
output += input;
|
|
11887
|
+
input = '';
|
|
11888
|
+
} else {
|
|
11889
|
+
output += input.slice(0, segmentEnd);
|
|
11890
|
+
input = input.slice(segmentEnd);
|
|
11891
|
+
}
|
|
11892
|
+
}
|
|
11893
|
+
}
|
|
11894
|
+
|
|
11895
|
+
return output;
|
|
11896
|
+
}
|
|
11897
|
+
|
|
11898
|
+
function mergePaths(base, refPath) {
|
|
11899
|
+
if (base.authority !== undefined && base.path === '') {
|
|
11900
|
+
return `/${refPath}`;
|
|
11901
|
+
}
|
|
11902
|
+
const i = base.path.lastIndexOf('/');
|
|
11903
|
+
if (i < 0) return refPath;
|
|
11904
|
+
return `${base.path.slice(0, i + 1)}${refPath}`;
|
|
11905
|
+
}
|
|
11906
|
+
|
|
11706
11907
|
function resolveIriRef(ref, base) {
|
|
11707
|
-
|
|
11708
|
-
if (
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11908
|
+
const r = parseUriReferenceForResolution(ref);
|
|
11909
|
+
if (!r) return ref;
|
|
11910
|
+
|
|
11911
|
+
const baseParts = base ? parseUriReferenceForResolution(base) : null;
|
|
11912
|
+
|
|
11913
|
+
// Absolute references do not need a base, but RFC 3986 section 5.2.2 still
|
|
11914
|
+
// applies remove_dot_segments(R.path) when R.scheme is defined.
|
|
11915
|
+
if (r.scheme !== undefined) {
|
|
11916
|
+
return recomposeUriReference({
|
|
11917
|
+
scheme: r.scheme,
|
|
11918
|
+
authority: r.authority,
|
|
11919
|
+
path: removeDotSegments(r.path),
|
|
11920
|
+
query: r.query,
|
|
11921
|
+
fragment: r.fragment,
|
|
11922
|
+
});
|
|
11923
|
+
}
|
|
11924
|
+
|
|
11925
|
+
// Without a usable base, preserve relative references as written.
|
|
11926
|
+
if (!baseParts || baseParts.scheme === undefined) return ref;
|
|
11927
|
+
|
|
11928
|
+
const t = {
|
|
11929
|
+
scheme: baseParts.scheme,
|
|
11930
|
+
authority: undefined,
|
|
11931
|
+
path: '',
|
|
11932
|
+
query: undefined,
|
|
11933
|
+
fragment: r.fragment,
|
|
11934
|
+
};
|
|
11935
|
+
|
|
11936
|
+
if (r.authority !== undefined) {
|
|
11937
|
+
t.authority = r.authority;
|
|
11938
|
+
t.path = removeDotSegments(r.path);
|
|
11939
|
+
t.query = r.query;
|
|
11940
|
+
} else if (r.path === '') {
|
|
11941
|
+
t.authority = baseParts.authority;
|
|
11942
|
+
t.path = baseParts.path;
|
|
11943
|
+
t.query = r.query !== undefined ? r.query : baseParts.query;
|
|
11944
|
+
} else {
|
|
11945
|
+
t.authority = baseParts.authority;
|
|
11946
|
+
if (r.path.startsWith('/')) {
|
|
11947
|
+
t.path = removeDotSegments(r.path);
|
|
11948
|
+
} else {
|
|
11949
|
+
t.path = removeDotSegments(mergePaths(baseParts, r.path));
|
|
11950
|
+
}
|
|
11951
|
+
t.query = r.query;
|
|
11713
11952
|
}
|
|
11953
|
+
|
|
11954
|
+
return recomposeUriReference(t);
|
|
11714
11955
|
}
|
|
11715
11956
|
|
|
11716
11957
|
// -----------------------------------------------------------------------------
|
package/lib/builtins.js
CHANGED
|
@@ -1680,15 +1680,85 @@ function __listElemsForBuiltin(listLike, facts) {
|
|
|
1680
1680
|
return null;
|
|
1681
1681
|
}
|
|
1682
1682
|
|
|
1683
|
-
function
|
|
1684
|
-
if (
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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(t);
|
|
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
|
-
!(
|
|
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;
|