eyeling 1.7.5 → 1.7.7
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/list-map.n3 +17 -0
- package/examples/log-conclusion.n3 +20 -0
- package/examples/output/list-map.n3 +6 -0
- package/examples/output/log-conclusion.n3 +13 -0
- package/eyeling.js +198 -13
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ========================================================
|
|
2
|
+
# List map
|
|
3
|
+
# behaves like the functional programming operator flatMap
|
|
4
|
+
# Examples from Giacomo Citi
|
|
5
|
+
# ========================================================
|
|
6
|
+
|
|
7
|
+
@prefix list: <http://www.w3.org/2000/10/swap/list#> .
|
|
8
|
+
@prefix : <http://example.org/#>.
|
|
9
|
+
|
|
10
|
+
:s1 :p1 :o1 .
|
|
11
|
+
:s2 :p1 :o2 .
|
|
12
|
+
:s3 :p1 :o3, :o4 .
|
|
13
|
+
|
|
14
|
+
{ ((:s1 :s2) :p1) list:map (:o1 :o2) } => { :test1 :is true } .
|
|
15
|
+
{ ((:s1 :s2 :s3) :p1) list:map (:o1 :o2 :o3 :o4) } => { :test2 :is true } .
|
|
16
|
+
{ ((:s4) :p1) list:map () } => { :test3 :is true } .
|
|
17
|
+
{ ((:s1) :p2) list:map () } => { :test4 :is true } .
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ======================
|
|
2
|
+
# log:conclusion example
|
|
3
|
+
# ======================
|
|
4
|
+
|
|
5
|
+
@prefix : <http://example.org/>.
|
|
6
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
7
|
+
|
|
8
|
+
:let :param {
|
|
9
|
+
:Felix a :Cat .
|
|
10
|
+
{ ?X a :Cat . } => { ?X :says "Meow" . } .
|
|
11
|
+
} .
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
:let :param ?param .
|
|
15
|
+
?param log:conclusion ?conclusion .
|
|
16
|
+
}
|
|
17
|
+
=>
|
|
18
|
+
{
|
|
19
|
+
:result :is ?conclusion .
|
|
20
|
+
} .
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@prefix : <http://example.org/> .
|
|
2
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
3
|
+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
4
|
+
|
|
5
|
+
:result :is {
|
|
6
|
+
:Felix a :Cat .
|
|
7
|
+
{
|
|
8
|
+
?X a :Cat .
|
|
9
|
+
} => {
|
|
10
|
+
?X :says "Meow" .
|
|
11
|
+
} .
|
|
12
|
+
:Felix :says "Meow" .
|
|
13
|
+
} .
|
package/eyeling.js
CHANGED
|
@@ -89,6 +89,62 @@ const jsonPointerCache = new Map();
|
|
|
89
89
|
// Key is the dereferenced document IRI *without* fragment.
|
|
90
90
|
const __logContentCache = new Map(); // iri -> string | null (null means fetch/read failed)
|
|
91
91
|
const __logSemanticsCache = new Map(); // iri -> GraphTerm | null (null means parse failed)
|
|
92
|
+
const __logConclusionCache = new WeakMap(); // GraphTerm -> GraphTerm (deductive closure)
|
|
93
|
+
|
|
94
|
+
// Environment detection (Node vs Browser/Worker).
|
|
95
|
+
// Eyeling is primarily synchronous, so we use sync XHR in browsers for log:content/log:semantics.
|
|
96
|
+
// Note: Browser fetches are subject to CORS; use CORS-enabled resources or a proxy.
|
|
97
|
+
const __IS_NODE =
|
|
98
|
+
typeof process !== 'undefined' &&
|
|
99
|
+
!!(process.versions && process.versions.node);
|
|
100
|
+
|
|
101
|
+
function __hasXmlHttpRequest() {
|
|
102
|
+
return typeof XMLHttpRequest !== 'undefined';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function __resolveBrowserUrl(ref) {
|
|
106
|
+
if (!ref) return ref;
|
|
107
|
+
// If already absolute, keep as-is.
|
|
108
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(ref)) return ref;
|
|
109
|
+
const base =
|
|
110
|
+
(typeof document !== 'undefined' && document.baseURI) ||
|
|
111
|
+
(typeof location !== 'undefined' && location.href) ||
|
|
112
|
+
'';
|
|
113
|
+
try {
|
|
114
|
+
return new URL(ref, base).toString();
|
|
115
|
+
} catch {
|
|
116
|
+
return ref;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function __fetchHttpTextSyncBrowser(url) {
|
|
121
|
+
if (!__hasXmlHttpRequest()) return null;
|
|
122
|
+
try {
|
|
123
|
+
const xhr = new XMLHttpRequest();
|
|
124
|
+
xhr.open('GET', url, false); // synchronous
|
|
125
|
+
try {
|
|
126
|
+
xhr.setRequestHeader(
|
|
127
|
+
'Accept',
|
|
128
|
+
'text/n3, text/turtle, application/n-triples, application/n-quads, text/plain;q=0.1, */*;q=0.01'
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
// Some environments restrict setting headers (ignore).
|
|
132
|
+
}
|
|
133
|
+
xhr.send(null);
|
|
134
|
+
const sc = xhr.status || 0;
|
|
135
|
+
if (sc < 200 || sc >= 300) return null;
|
|
136
|
+
return xhr.responseText;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function __normalizeDerefIri(iriNoFrag) {
|
|
143
|
+
// In Node, treat non-http as local path; leave as-is.
|
|
144
|
+
if (__IS_NODE) return iriNoFrag;
|
|
145
|
+
// In browsers/workers, resolve relative references against the page URL.
|
|
146
|
+
return __resolveBrowserUrl(iriNoFrag);
|
|
147
|
+
}
|
|
92
148
|
|
|
93
149
|
function __stripFragment(iri) {
|
|
94
150
|
const i = iri.indexOf('#');
|
|
@@ -114,6 +170,7 @@ function __fileIriToPath(fileIri) {
|
|
|
114
170
|
}
|
|
115
171
|
|
|
116
172
|
function __readFileText(pathOrFileIri) {
|
|
173
|
+
if (!__IS_NODE) return null;
|
|
117
174
|
const fs = require('fs');
|
|
118
175
|
let path = pathOrFileIri;
|
|
119
176
|
if (__isFileIri(pathOrFileIri)) path = __fileIriToPath(pathOrFileIri);
|
|
@@ -125,6 +182,7 @@ function __readFileText(pathOrFileIri) {
|
|
|
125
182
|
}
|
|
126
183
|
|
|
127
184
|
function __fetchHttpTextViaSubprocess(url) {
|
|
185
|
+
if (!__IS_NODE) return null;
|
|
128
186
|
const cp = require('child_process');
|
|
129
187
|
// Use a subprocess so this code remains synchronous without rewriting the whole reasoner to async.
|
|
130
188
|
const script = `
|
|
@@ -180,16 +238,27 @@ function __fetchHttpTextViaSubprocess(url) {
|
|
|
180
238
|
}
|
|
181
239
|
|
|
182
240
|
function __derefTextSync(iriNoFrag) {
|
|
183
|
-
|
|
241
|
+
const norm = __normalizeDerefIri(iriNoFrag);
|
|
242
|
+
const key = typeof norm === 'string' && norm ? norm : iriNoFrag;
|
|
243
|
+
|
|
244
|
+
if (__logContentCache.has(key)) return __logContentCache.get(key);
|
|
184
245
|
|
|
185
246
|
let text = null;
|
|
186
|
-
|
|
187
|
-
|
|
247
|
+
|
|
248
|
+
if (__IS_NODE) {
|
|
249
|
+
if (__isHttpIri(key)) {
|
|
250
|
+
text = __fetchHttpTextViaSubprocess(key);
|
|
251
|
+
} else {
|
|
252
|
+
// Treat any non-http(s) IRI as a local path (including file://), for basic usability.
|
|
253
|
+
text = __readFileText(key);
|
|
254
|
+
}
|
|
188
255
|
} else {
|
|
189
|
-
//
|
|
190
|
-
|
|
256
|
+
// Browser / Worker: we can only dereference over HTTP(S), and it must pass CORS.
|
|
257
|
+
const url = typeof norm === 'string' && norm ? norm : key;
|
|
258
|
+
if (__isHttpIri(url)) text = __fetchHttpTextSyncBrowser(url);
|
|
191
259
|
}
|
|
192
|
-
|
|
260
|
+
|
|
261
|
+
__logContentCache.set(key, text);
|
|
193
262
|
return text;
|
|
194
263
|
}
|
|
195
264
|
|
|
@@ -217,22 +286,106 @@ function __parseSemanticsToFormula(text, baseIri) {
|
|
|
217
286
|
}
|
|
218
287
|
|
|
219
288
|
function __derefSemanticsSync(iriNoFrag) {
|
|
220
|
-
|
|
289
|
+
const norm = __normalizeDerefIri(iriNoFrag);
|
|
290
|
+
const key = typeof norm === 'string' && norm ? norm : iriNoFrag;
|
|
291
|
+
if (__logSemanticsCache.has(key)) return __logSemanticsCache.get(key);
|
|
221
292
|
|
|
222
293
|
const text = __derefTextSync(iriNoFrag);
|
|
223
294
|
if (typeof text !== 'string') {
|
|
224
|
-
__logSemanticsCache.set(
|
|
295
|
+
__logSemanticsCache.set(key, null);
|
|
225
296
|
return null;
|
|
226
297
|
}
|
|
227
298
|
try {
|
|
228
|
-
const
|
|
229
|
-
|
|
299
|
+
const baseIri = (typeof key === 'string' && key) ? key : iriNoFrag;
|
|
300
|
+
const formula = __parseSemanticsToFormula(text, baseIri);
|
|
301
|
+
__logSemanticsCache.set(key, formula);
|
|
230
302
|
return formula;
|
|
231
303
|
} catch {
|
|
232
|
-
__logSemanticsCache.set(
|
|
304
|
+
__logSemanticsCache.set(key, null);
|
|
233
305
|
return null;
|
|
234
306
|
}
|
|
235
307
|
}
|
|
308
|
+
function __makeRuleFromTerms(left, right, isForward) {
|
|
309
|
+
// Mirror Parser.makeRule, but usable at runtime (e.g., log:conclusion).
|
|
310
|
+
let premiseTerm, conclTerm;
|
|
311
|
+
|
|
312
|
+
if (isForward) {
|
|
313
|
+
premiseTerm = left;
|
|
314
|
+
conclTerm = right;
|
|
315
|
+
} else {
|
|
316
|
+
premiseTerm = right;
|
|
317
|
+
conclTerm = left;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
let isFuse = false;
|
|
321
|
+
if (isForward) {
|
|
322
|
+
if (conclTerm instanceof Literal && conclTerm.value === 'false') {
|
|
323
|
+
isFuse = true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let rawPremise;
|
|
328
|
+
if (premiseTerm instanceof GraphTerm) {
|
|
329
|
+
rawPremise = premiseTerm.triples;
|
|
330
|
+
} else if (premiseTerm instanceof Literal && premiseTerm.value === 'true') {
|
|
331
|
+
rawPremise = [];
|
|
332
|
+
} else {
|
|
333
|
+
rawPremise = [];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let rawConclusion;
|
|
337
|
+
if (conclTerm instanceof GraphTerm) {
|
|
338
|
+
rawConclusion = conclTerm.triples;
|
|
339
|
+
} else if (conclTerm instanceof Literal && conclTerm.value === 'false') {
|
|
340
|
+
rawConclusion = [];
|
|
341
|
+
} else {
|
|
342
|
+
rawConclusion = [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
|
|
346
|
+
const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
347
|
+
const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
|
|
348
|
+
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function __computeConclusionFromFormula(formula) {
|
|
352
|
+
if (!(formula instanceof GraphTerm)) return null;
|
|
353
|
+
|
|
354
|
+
const cached = __logConclusionCache.get(formula);
|
|
355
|
+
if (cached) return cached;
|
|
356
|
+
|
|
357
|
+
// Facts start as *all* triples in the formula, including rule triples.
|
|
358
|
+
const facts2 = formula.triples.slice();
|
|
359
|
+
|
|
360
|
+
// Extract rules from rule-triples present inside the formula.
|
|
361
|
+
const fw = [];
|
|
362
|
+
const bw = [];
|
|
363
|
+
|
|
364
|
+
for (const tr of formula.triples) {
|
|
365
|
+
// Treat {A} => {B} as a forward rule.
|
|
366
|
+
if (isLogImplies(tr.p)) {
|
|
367
|
+
fw.push(__makeRuleFromTerms(tr.s, tr.o, true));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Treat {A} <= {B} as the same rule in the other direction, i.e., {B} => {A},
|
|
372
|
+
// so it participates in deductive closure even if only <= is used.
|
|
373
|
+
if (isLogImpliedBy(tr.p)) {
|
|
374
|
+
fw.push(__makeRuleFromTerms(tr.o, tr.s, true));
|
|
375
|
+
// Also index it as a backward rule for completeness (helps proveGoals in some cases).
|
|
376
|
+
bw.push(__makeRuleFromTerms(tr.s, tr.o, false));
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Saturate within this local formula only.
|
|
382
|
+
forwardChain(facts2, fw, bw);
|
|
383
|
+
|
|
384
|
+
const out = new GraphTerm(facts2.slice());
|
|
385
|
+
__logConclusionCache.set(formula, out);
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
236
389
|
|
|
237
390
|
|
|
238
391
|
// Controls whether human-readable proof comments are printed.
|
|
@@ -4850,6 +5003,31 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
|
|
|
4850
5003
|
return s2 !== null ? [s2] : [];
|
|
4851
5004
|
}
|
|
4852
5005
|
|
|
5006
|
+
|
|
5007
|
+
// log:conclusion
|
|
5008
|
+
// Schema: $s+ log:conclusion $o?
|
|
5009
|
+
// $o is the deductive closure of the subject formula $s (including rule inferences).
|
|
5010
|
+
if (pv === LOG_NS + 'conclusion') {
|
|
5011
|
+
// Accept 'true' as the empty formula.
|
|
5012
|
+
let inFormula = null;
|
|
5013
|
+
if (g.s instanceof GraphTerm) inFormula = g.s;
|
|
5014
|
+
else if (g.s instanceof Literal && g.s.value === 'true') inFormula = new GraphTerm([]);
|
|
5015
|
+
else return [];
|
|
5016
|
+
|
|
5017
|
+
const conclusion = __computeConclusionFromFormula(inFormula);
|
|
5018
|
+
if (!(conclusion instanceof GraphTerm)) return [];
|
|
5019
|
+
|
|
5020
|
+
if (g.o instanceof Var) {
|
|
5021
|
+
const s2 = { ...subst };
|
|
5022
|
+
s2[g.o.name] = conclusion;
|
|
5023
|
+
return [s2];
|
|
5024
|
+
}
|
|
5025
|
+
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5026
|
+
|
|
5027
|
+
const s2 = unifyTerm(g.o, conclusion, subst);
|
|
5028
|
+
return s2 !== null ? [s2] : [];
|
|
5029
|
+
}
|
|
5030
|
+
|
|
4853
5031
|
// log:content
|
|
4854
5032
|
// Schema: $s+ log:content $o?
|
|
4855
5033
|
// Dereferences $s and returns the online resource as an xsd:string.
|
|
@@ -5998,10 +6176,17 @@ function termToN3(t, pref) {
|
|
|
5998
6176
|
return '(' + inside.join(' ') + ')';
|
|
5999
6177
|
}
|
|
6000
6178
|
if (t instanceof GraphTerm) {
|
|
6179
|
+
const indent = ' ';
|
|
6180
|
+
const indentBlock = (str) =>
|
|
6181
|
+
str
|
|
6182
|
+
.split(/\r?\n/)
|
|
6183
|
+
.map((ln) => (ln.length ? indent + ln : ln))
|
|
6184
|
+
.join('\n');
|
|
6185
|
+
|
|
6001
6186
|
let s = '{\n';
|
|
6002
6187
|
for (const tr of t.triples) {
|
|
6003
|
-
|
|
6004
|
-
if (
|
|
6188
|
+
const block = tripleToN3(tr, pref).trimEnd();
|
|
6189
|
+
if (block) s += indentBlock(block) + '\n';
|
|
6005
6190
|
}
|
|
6006
6191
|
s += '}';
|
|
6007
6192
|
return s;
|