eyeling 1.5.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/LICENSE.md +21 -0
- package/README.md +188 -0
- package/eyeling.js +3945 -0
- package/package.json +27 -0
package/eyeling.js
ADDED
|
@@ -0,0 +1,3945 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* eyeling.js — a minimal Notation3 (N3) reasoner in JavaScript
|
|
6
|
+
*
|
|
7
|
+
* High-level pipeline:
|
|
8
|
+
* 1) Read an N3 file from disk.
|
|
9
|
+
* 2) Lex it into Tokens.
|
|
10
|
+
* 3) Parse tokens into:
|
|
11
|
+
* - ground triples (facts)
|
|
12
|
+
* - forward rules {premise} => {conclusion}.
|
|
13
|
+
* - backward rules {head} <= {body}.
|
|
14
|
+
* 4) Run forward chaining to fixpoint.
|
|
15
|
+
* - premises are proven using backward rules + builtins.
|
|
16
|
+
* 5) Print only newly derived forward facts with explanations.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { version } = require('./package.json');
|
|
20
|
+
const nodeCrypto = require("crypto");
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Namespace constants
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
|
|
27
|
+
const RDFS_NS = "http://www.w3.org/2000/01/rdf-schema#";
|
|
28
|
+
const OWL_NS = "http://www.w3.org/2002/07/owl#";
|
|
29
|
+
const XSD_NS = "http://www.w3.org/2001/XMLSchema#";
|
|
30
|
+
const CRYPTO_NS = "http://www.w3.org/2000/10/swap/crypto#";
|
|
31
|
+
const MATH_NS = "http://www.w3.org/2000/10/swap/math#";
|
|
32
|
+
const TIME_NS = "http://www.w3.org/2000/10/swap/time#";
|
|
33
|
+
const LIST_NS = "http://www.w3.org/2000/10/swap/list#";
|
|
34
|
+
const LOG_NS = "http://www.w3.org/2000/10/swap/log#";
|
|
35
|
+
const STRING_NS = "http://www.w3.org/2000/10/swap/string#";
|
|
36
|
+
const SKOLEM_NS = "https://eyereasoner.github.io/.well-known/genid/";
|
|
37
|
+
|
|
38
|
+
// For a single reasoning run, this maps a canonical representation
|
|
39
|
+
// of the subject term in log:skolem to a Skolem IRI.
|
|
40
|
+
const skolemCache = new Map();
|
|
41
|
+
|
|
42
|
+
// Controls whether human-readable proof comments are printed.
|
|
43
|
+
let proofCommentsEnabled = true;
|
|
44
|
+
|
|
45
|
+
// Deterministic pseudo-UUID from a string key (for log:skolem).
|
|
46
|
+
// Not cryptographically strong, but stable and platform-independent.
|
|
47
|
+
function deterministicSkolemIdFromKey(key) {
|
|
48
|
+
// Four 32-bit FNV-1a style accumulators with slight variation
|
|
49
|
+
let h1 = 0x811c9dc5;
|
|
50
|
+
let h2 = 0x811c9dc5;
|
|
51
|
+
let h3 = 0x811c9dc5;
|
|
52
|
+
let h4 = 0x811c9dc5;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < key.length; i++) {
|
|
55
|
+
const c = key.charCodeAt(i);
|
|
56
|
+
|
|
57
|
+
h1 ^= c;
|
|
58
|
+
h1 = (h1 * 0x01000193) >>> 0;
|
|
59
|
+
|
|
60
|
+
h2 ^= c + 1;
|
|
61
|
+
h2 = (h2 * 0x01000193) >>> 0;
|
|
62
|
+
|
|
63
|
+
h3 ^= c + 2;
|
|
64
|
+
h3 = (h3 * 0x01000193) >>> 0;
|
|
65
|
+
|
|
66
|
+
h4 ^= c + 3;
|
|
67
|
+
h4 = (h4 * 0x01000193) >>> 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const hex = [h1, h2, h3, h4]
|
|
71
|
+
.map(h => h.toString(16).padStart(8, "0"))
|
|
72
|
+
.join(""); // 32 hex chars
|
|
73
|
+
|
|
74
|
+
// Format like a UUID: 8-4-4-4-12
|
|
75
|
+
return (
|
|
76
|
+
hex.slice(0, 8) + "-" +
|
|
77
|
+
hex.slice(8, 12) + "-" +
|
|
78
|
+
hex.slice(12, 16) + "-" +
|
|
79
|
+
hex.slice(16, 20) + "-" +
|
|
80
|
+
hex.slice(20)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// AST (Abstract Syntax Tree)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
class Term {}
|
|
89
|
+
|
|
90
|
+
class Iri extends Term {
|
|
91
|
+
constructor(value) {
|
|
92
|
+
super();
|
|
93
|
+
this.value = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class Literal extends Term {
|
|
98
|
+
constructor(value) {
|
|
99
|
+
super();
|
|
100
|
+
this.value = value; // raw lexical form, e.g. "foo", 12, true, or "\"1944-08-21\"^^..."
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class Var extends Term {
|
|
105
|
+
constructor(name) {
|
|
106
|
+
super();
|
|
107
|
+
this.name = name; // without leading '?'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class Blank extends Term {
|
|
112
|
+
constructor(label) {
|
|
113
|
+
super();
|
|
114
|
+
this.label = label; // _:b1, etc.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class ListTerm extends Term {
|
|
119
|
+
constructor(elems) {
|
|
120
|
+
super();
|
|
121
|
+
this.elems = elems; // Term[]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class OpenListTerm extends Term {
|
|
126
|
+
constructor(prefix, tailVar) {
|
|
127
|
+
super();
|
|
128
|
+
this.prefix = prefix; // Term[]
|
|
129
|
+
this.tailVar = tailVar; // string
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class FormulaTerm extends Term {
|
|
134
|
+
constructor(triples) {
|
|
135
|
+
super();
|
|
136
|
+
this.triples = triples; // Triple[]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
class Triple {
|
|
141
|
+
constructor(s, p, o) {
|
|
142
|
+
this.s = s;
|
|
143
|
+
this.p = p;
|
|
144
|
+
this.o = o;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class Rule {
|
|
149
|
+
constructor(premise, conclusion, isForward, isFuse, headBlankLabels) {
|
|
150
|
+
this.premise = premise; // Triple[]
|
|
151
|
+
this.conclusion = conclusion; // Triple[]
|
|
152
|
+
this.isForward = isForward; // boolean
|
|
153
|
+
this.isFuse = isFuse; // boolean
|
|
154
|
+
// Set<string> of blank-node labels that occur explicitly in the rule head
|
|
155
|
+
this.headBlankLabels = headBlankLabels || new Set();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class DerivedFact {
|
|
160
|
+
constructor(fact, rule, premises, subst) {
|
|
161
|
+
this.fact = fact; // Triple
|
|
162
|
+
this.rule = rule; // Rule
|
|
163
|
+
this.premises = premises; // Triple[]
|
|
164
|
+
this.subst = subst; // { varName: Term }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================================
|
|
169
|
+
// LEXER
|
|
170
|
+
// ============================================================================
|
|
171
|
+
|
|
172
|
+
class Token {
|
|
173
|
+
constructor(typ, value = null) {
|
|
174
|
+
this.typ = typ;
|
|
175
|
+
this.value = value;
|
|
176
|
+
}
|
|
177
|
+
toString() {
|
|
178
|
+
if (this.value == null) return `Token(${this.typ})`;
|
|
179
|
+
return `Token(${this.typ}, ${JSON.stringify(this.value)})`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isWs(c) {
|
|
184
|
+
return /\s/.test(c);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isNameChar(c) {
|
|
188
|
+
return /[0-9A-Za-z_\-:]/.test(c);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function lex(inputText) {
|
|
192
|
+
const chars = Array.from(inputText);
|
|
193
|
+
const n = chars.length;
|
|
194
|
+
let i = 0;
|
|
195
|
+
const tokens = [];
|
|
196
|
+
|
|
197
|
+
function peek(offset = 0) {
|
|
198
|
+
const j = i + offset;
|
|
199
|
+
return j >= 0 && j < n ? chars[j] : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
while (i < n) {
|
|
203
|
+
let c = peek();
|
|
204
|
+
if (c === null) break;
|
|
205
|
+
|
|
206
|
+
// 1) Whitespace
|
|
207
|
+
if (isWs(c)) {
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 2) Comments starting with '#'
|
|
213
|
+
if (c === "#") {
|
|
214
|
+
while (i < n && chars[i] !== "\n" && chars[i] !== "\r") i++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3) Two-character operators: => and <=
|
|
219
|
+
if (c === "=") {
|
|
220
|
+
if (peek(1) === ">") {
|
|
221
|
+
tokens.push(new Token("OpImplies"));
|
|
222
|
+
i += 2;
|
|
223
|
+
continue;
|
|
224
|
+
} else {
|
|
225
|
+
// N3 syntactic sugar: '=' means owl:sameAs
|
|
226
|
+
tokens.push(new Token("Equals"));
|
|
227
|
+
i += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (c === "<") {
|
|
233
|
+
if (peek(1) === "=") {
|
|
234
|
+
tokens.push(new Token("OpImpliedBy"));
|
|
235
|
+
i += 2;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Otherwise IRIREF <...>
|
|
239
|
+
i++; // skip '<'
|
|
240
|
+
const iriChars = [];
|
|
241
|
+
while (i < n && chars[i] !== ">") {
|
|
242
|
+
iriChars.push(chars[i]);
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
if (i >= n || chars[i] !== ">") {
|
|
246
|
+
throw new Error("Unterminated IRI <...>");
|
|
247
|
+
}
|
|
248
|
+
i++; // skip '>'
|
|
249
|
+
const iri = iriChars.join("");
|
|
250
|
+
tokens.push(new Token("IriRef", iri));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 4) Datatype operator ^^
|
|
255
|
+
if (c === "^") {
|
|
256
|
+
if (peek(1) === "^") {
|
|
257
|
+
tokens.push(new Token("HatHat"));
|
|
258
|
+
i += 2;
|
|
259
|
+
continue;
|
|
260
|
+
} else {
|
|
261
|
+
throw new Error("Unexpected '^' (did you mean ^^?)");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 5) Single-character punctuation
|
|
266
|
+
if ("{}()[];,.".includes(c)) {
|
|
267
|
+
const mapping = {
|
|
268
|
+
"{": "LBrace",
|
|
269
|
+
"}": "RBrace",
|
|
270
|
+
"(": "LParen",
|
|
271
|
+
")": "RParen",
|
|
272
|
+
"[": "LBracket",
|
|
273
|
+
"]": "RBracket",
|
|
274
|
+
";": "Semicolon",
|
|
275
|
+
",": "Comma",
|
|
276
|
+
".": "Dot",
|
|
277
|
+
};
|
|
278
|
+
tokens.push(new Token(mapping[c]));
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// String literal
|
|
284
|
+
if (c === '"') {
|
|
285
|
+
i++; // consume opening "
|
|
286
|
+
const sChars = [];
|
|
287
|
+
while (i < n) {
|
|
288
|
+
let cc = chars[i];
|
|
289
|
+
i++;
|
|
290
|
+
if (cc === "\\") {
|
|
291
|
+
if (i < n) {
|
|
292
|
+
const esc = chars[i];
|
|
293
|
+
i++;
|
|
294
|
+
sChars.push("\\");
|
|
295
|
+
sChars.push(esc);
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (cc === '"') break;
|
|
300
|
+
sChars.push(cc);
|
|
301
|
+
}
|
|
302
|
+
const s = '"' + sChars.join("") + '"';
|
|
303
|
+
tokens.push(new Token("Literal", s));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Variable ?name
|
|
308
|
+
if (c === "?") {
|
|
309
|
+
i++;
|
|
310
|
+
const nameChars = [];
|
|
311
|
+
let cc;
|
|
312
|
+
while ((cc = peek()) !== null && isNameChar(cc)) {
|
|
313
|
+
nameChars.push(cc);
|
|
314
|
+
i++;
|
|
315
|
+
}
|
|
316
|
+
const name = nameChars.join("");
|
|
317
|
+
tokens.push(new Token("Var", name));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Directives: @prefix, @base
|
|
322
|
+
if (c === "@") {
|
|
323
|
+
i++;
|
|
324
|
+
const wordChars = [];
|
|
325
|
+
let cc;
|
|
326
|
+
while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
|
|
327
|
+
wordChars.push(cc);
|
|
328
|
+
i++;
|
|
329
|
+
}
|
|
330
|
+
const word = wordChars.join("");
|
|
331
|
+
if (word === "prefix") tokens.push(new Token("AtPrefix"));
|
|
332
|
+
else if (word === "base") tokens.push(new Token("AtBase"));
|
|
333
|
+
else throw new Error(`Unknown directive @${word}`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 6) Numeric literal (integer or float)
|
|
338
|
+
if (/[0-9]/.test(c) || (c === "-" && peek(1) !== null && /[0-9]/.test(peek(1)))) {
|
|
339
|
+
const numChars = [c];
|
|
340
|
+
i++;
|
|
341
|
+
while (i < n) {
|
|
342
|
+
const cc = chars[i];
|
|
343
|
+
if (/[0-9]/.test(cc)) {
|
|
344
|
+
numChars.push(cc);
|
|
345
|
+
i++;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (cc === ".") {
|
|
349
|
+
if (i + 1 < n && /[0-9]/.test(chars[i + 1])) {
|
|
350
|
+
numChars.push(".");
|
|
351
|
+
i++;
|
|
352
|
+
continue;
|
|
353
|
+
} else {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
tokens.push(new Token("Literal", numChars.join("")));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// 7) Identifiers / keywords / QNames
|
|
364
|
+
const wordChars = [];
|
|
365
|
+
let cc;
|
|
366
|
+
while ((cc = peek()) !== null && isNameChar(cc)) {
|
|
367
|
+
wordChars.push(cc);
|
|
368
|
+
i++;
|
|
369
|
+
}
|
|
370
|
+
if (!wordChars.length) {
|
|
371
|
+
throw new Error(`Unexpected char: ${JSON.stringify(c)}`);
|
|
372
|
+
}
|
|
373
|
+
const word = wordChars.join("");
|
|
374
|
+
if (word === "true" || word === "false") {
|
|
375
|
+
tokens.push(new Token("Literal", word));
|
|
376
|
+
} else if ([...word].every(ch => /[0-9.\-]/.test(ch))) {
|
|
377
|
+
tokens.push(new Token("Literal", word));
|
|
378
|
+
} else {
|
|
379
|
+
tokens.push(new Token("Ident", word));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
tokens.push(new Token("EOF"));
|
|
384
|
+
return tokens;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============================================================================
|
|
388
|
+
// PREFIX ENVIRONMENT
|
|
389
|
+
// ============================================================================
|
|
390
|
+
|
|
391
|
+
class PrefixEnv {
|
|
392
|
+
constructor(map) {
|
|
393
|
+
this.map = map || {}; // prefix -> IRI
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
static newDefault() {
|
|
397
|
+
const m = {};
|
|
398
|
+
m["rdf"] = RDF_NS;
|
|
399
|
+
m["rdfs"] = RDFS_NS;
|
|
400
|
+
m["xsd"] = XSD_NS;
|
|
401
|
+
m["log"] = LOG_NS;
|
|
402
|
+
m["math"] = MATH_NS;
|
|
403
|
+
m["string"] = STRING_NS;
|
|
404
|
+
m["list"] = LIST_NS;
|
|
405
|
+
m["time"] = TIME_NS;
|
|
406
|
+
m[""] = "";
|
|
407
|
+
return new PrefixEnv(m);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
set(pref, base) {
|
|
411
|
+
this.map[pref] = base;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
expandQName(q) {
|
|
415
|
+
if (q.includes(":")) {
|
|
416
|
+
const [p, local] = q.split(":", 2);
|
|
417
|
+
const base = this.map[p] || "";
|
|
418
|
+
if (base) return base + local;
|
|
419
|
+
return q;
|
|
420
|
+
}
|
|
421
|
+
return q;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
shrinkIri(iri) {
|
|
425
|
+
let best = null; // [prefix, local]
|
|
426
|
+
for (const [p, base] of Object.entries(this.map)) {
|
|
427
|
+
if (!base) continue;
|
|
428
|
+
if (iri.startsWith(base)) {
|
|
429
|
+
const local = iri.slice(base.length);
|
|
430
|
+
if (!local) continue;
|
|
431
|
+
const cand = [p, local];
|
|
432
|
+
if (best === null || cand[1].length < best[1].length) best = cand;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (best === null) return null;
|
|
436
|
+
const [p, local] = best;
|
|
437
|
+
if (p === "") return `:${local}`;
|
|
438
|
+
return `${p}:${local}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
prefixesUsedForOutput(triples) {
|
|
442
|
+
const used = new Set();
|
|
443
|
+
for (const t of triples) {
|
|
444
|
+
const iris = [];
|
|
445
|
+
iris.push(...collectIrisInTerm(t.s));
|
|
446
|
+
if (!isRdfTypePred(t.p)) {
|
|
447
|
+
iris.push(...collectIrisInTerm(t.p));
|
|
448
|
+
}
|
|
449
|
+
iris.push(...collectIrisInTerm(t.o));
|
|
450
|
+
for (const iri of iris) {
|
|
451
|
+
for (const [p, base] of Object.entries(this.map)) {
|
|
452
|
+
if (base && iri.startsWith(base)) used.add(p);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const v = [];
|
|
457
|
+
for (const p of used) {
|
|
458
|
+
if (this.map.hasOwnProperty(p)) v.push([p, this.map[p]]);
|
|
459
|
+
}
|
|
460
|
+
v.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
461
|
+
return v;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function collectIrisInTerm(t) {
|
|
466
|
+
const out = [];
|
|
467
|
+
if (t instanceof Iri) {
|
|
468
|
+
out.push(t.value);
|
|
469
|
+
} else if (t instanceof ListTerm) {
|
|
470
|
+
for (const x of t.elems) out.push(...collectIrisInTerm(x));
|
|
471
|
+
} else if (t instanceof OpenListTerm) {
|
|
472
|
+
for (const x of t.prefix) out.push(...collectIrisInTerm(x));
|
|
473
|
+
} else if (t instanceof FormulaTerm) {
|
|
474
|
+
for (const tr of t.triples) {
|
|
475
|
+
out.push(...collectIrisInTerm(tr.s));
|
|
476
|
+
out.push(...collectIrisInTerm(tr.p));
|
|
477
|
+
out.push(...collectIrisInTerm(tr.o));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectVarsInTerm(t, acc) {
|
|
484
|
+
if (t instanceof Var) {
|
|
485
|
+
acc.add(t.name);
|
|
486
|
+
} else if (t instanceof ListTerm) {
|
|
487
|
+
for (const x of t.elems) collectVarsInTerm(x, acc);
|
|
488
|
+
} else if (t instanceof OpenListTerm) {
|
|
489
|
+
for (const x of t.prefix) collectVarsInTerm(x, acc);
|
|
490
|
+
acc.add(t.tailVar);
|
|
491
|
+
} else if (t instanceof FormulaTerm) {
|
|
492
|
+
for (const tr of t.triples) {
|
|
493
|
+
collectVarsInTerm(tr.s, acc);
|
|
494
|
+
collectVarsInTerm(tr.p, acc);
|
|
495
|
+
collectVarsInTerm(tr.o, acc);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function varsInRule(rule) {
|
|
501
|
+
const acc = new Set();
|
|
502
|
+
for (const tr of rule.premise) {
|
|
503
|
+
collectVarsInTerm(tr.s, acc);
|
|
504
|
+
collectVarsInTerm(tr.p, acc);
|
|
505
|
+
collectVarsInTerm(tr.o, acc);
|
|
506
|
+
}
|
|
507
|
+
for (const tr of rule.conclusion) {
|
|
508
|
+
collectVarsInTerm(tr.s, acc);
|
|
509
|
+
collectVarsInTerm(tr.p, acc);
|
|
510
|
+
collectVarsInTerm(tr.o, acc);
|
|
511
|
+
}
|
|
512
|
+
return acc;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function collectBlankLabelsInTerm(t, acc) {
|
|
516
|
+
if (t instanceof Blank) {
|
|
517
|
+
acc.add(t.label);
|
|
518
|
+
} else if (t instanceof ListTerm) {
|
|
519
|
+
for (const x of t.elems) collectBlankLabelsInTerm(x, acc);
|
|
520
|
+
} else if (t instanceof OpenListTerm) {
|
|
521
|
+
for (const x of t.prefix) collectBlankLabelsInTerm(x, acc);
|
|
522
|
+
} else if (t instanceof FormulaTerm) {
|
|
523
|
+
for (const tr of t.triples) {
|
|
524
|
+
collectBlankLabelsInTerm(tr.s, acc);
|
|
525
|
+
collectBlankLabelsInTerm(tr.p, acc);
|
|
526
|
+
collectBlankLabelsInTerm(tr.o, acc);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function collectBlankLabelsInTriples(triples) {
|
|
532
|
+
const acc = new Set();
|
|
533
|
+
for (const tr of triples) {
|
|
534
|
+
collectBlankLabelsInTerm(tr.s, acc);
|
|
535
|
+
collectBlankLabelsInTerm(tr.p, acc);
|
|
536
|
+
collectBlankLabelsInTerm(tr.o, acc);
|
|
537
|
+
}
|
|
538
|
+
return acc;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ============================================================================
|
|
542
|
+
// PARSER
|
|
543
|
+
// ============================================================================
|
|
544
|
+
|
|
545
|
+
class Parser {
|
|
546
|
+
constructor(tokens) {
|
|
547
|
+
this.toks = tokens;
|
|
548
|
+
this.pos = 0;
|
|
549
|
+
this.prefixes = PrefixEnv.newDefault();
|
|
550
|
+
this.blankCounter = 0;
|
|
551
|
+
this.pendingTriples = [];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
peek() {
|
|
555
|
+
return this.toks[this.pos];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
next() {
|
|
559
|
+
const tok = this.toks[this.pos];
|
|
560
|
+
this.pos += 1;
|
|
561
|
+
return tok;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
expectDot() {
|
|
565
|
+
const tok = this.next();
|
|
566
|
+
if (tok.typ !== "Dot") {
|
|
567
|
+
throw new Error(`Expected '.', got ${tok.toString()}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
parseDocument() {
|
|
572
|
+
const triples = [];
|
|
573
|
+
const forwardRules = [];
|
|
574
|
+
const backwardRules = [];
|
|
575
|
+
|
|
576
|
+
while (this.peek().typ !== "EOF") {
|
|
577
|
+
if (this.peek().typ === "AtPrefix") {
|
|
578
|
+
this.next();
|
|
579
|
+
this.parsePrefixDirective();
|
|
580
|
+
} else if (this.peek().typ === "AtBase") {
|
|
581
|
+
this.next();
|
|
582
|
+
this.parseBaseDirective();
|
|
583
|
+
} else {
|
|
584
|
+
const first = this.parseTerm();
|
|
585
|
+
if (this.peek().typ === "OpImplies") {
|
|
586
|
+
this.next();
|
|
587
|
+
const second = this.parseTerm();
|
|
588
|
+
this.expectDot();
|
|
589
|
+
forwardRules.push(this.makeRule(first, second, true));
|
|
590
|
+
} else if (this.peek().typ === "OpImpliedBy") {
|
|
591
|
+
this.next();
|
|
592
|
+
const second = this.parseTerm();
|
|
593
|
+
this.expectDot();
|
|
594
|
+
backwardRules.push(this.makeRule(first, second, false));
|
|
595
|
+
} else {
|
|
596
|
+
const more = this.parsePredicateObjectList(first);
|
|
597
|
+
this.expectDot();
|
|
598
|
+
// normalize explicit log:implies / log:impliedBy at top-level
|
|
599
|
+
for (const tr of more) {
|
|
600
|
+
if (isLogImplies(tr.p) && tr.s instanceof FormulaTerm && tr.o instanceof FormulaTerm) {
|
|
601
|
+
forwardRules.push(this.makeRule(tr.s, tr.o, true));
|
|
602
|
+
} else if (isLogImpliedBy(tr.p) && tr.s instanceof FormulaTerm && tr.o instanceof FormulaTerm) {
|
|
603
|
+
backwardRules.push(this.makeRule(tr.s, tr.o, false));
|
|
604
|
+
} else {
|
|
605
|
+
triples.push(tr);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// console.log(JSON.stringify([this.prefixes, triples, forwardRules, backwardRules], null, 2));
|
|
613
|
+
return [this.prefixes, triples, forwardRules, backwardRules];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
parsePrefixDirective() {
|
|
617
|
+
const tok = this.next();
|
|
618
|
+
if (tok.typ !== "Ident") {
|
|
619
|
+
throw new Error(`Expected prefix name, got ${tok.toString()}`);
|
|
620
|
+
}
|
|
621
|
+
const pref = tok.value || "";
|
|
622
|
+
const prefName = pref.endsWith(":") ? pref.slice(0, -1) : pref;
|
|
623
|
+
|
|
624
|
+
if (this.peek().typ === "Dot") {
|
|
625
|
+
this.next();
|
|
626
|
+
if (!this.prefixes.map.hasOwnProperty(prefName)) {
|
|
627
|
+
this.prefixes.set(prefName, "");
|
|
628
|
+
}
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const tok2 = this.next();
|
|
633
|
+
let iri;
|
|
634
|
+
if (tok2.typ === "IriRef") {
|
|
635
|
+
iri = tok2.value || "";
|
|
636
|
+
} else if (tok2.typ === "Ident") {
|
|
637
|
+
iri = this.prefixes.expandQName(tok2.value || "");
|
|
638
|
+
} else {
|
|
639
|
+
throw new Error(`Expected IRI after @prefix, got ${tok2.toString()}`);
|
|
640
|
+
}
|
|
641
|
+
this.expectDot();
|
|
642
|
+
this.prefixes.set(prefName, iri);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
parseBaseDirective() {
|
|
646
|
+
const tok = this.next();
|
|
647
|
+
let iri;
|
|
648
|
+
if (tok.typ === "IriRef") {
|
|
649
|
+
iri = tok.value || "";
|
|
650
|
+
} else if (tok.typ === "Ident") {
|
|
651
|
+
iri = tok.value || "";
|
|
652
|
+
} else {
|
|
653
|
+
throw new Error(`Expected IRI after @base, got ${tok.toString()}`);
|
|
654
|
+
}
|
|
655
|
+
this.expectDot();
|
|
656
|
+
this.prefixes.set("", iri);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
parseTerm() {
|
|
660
|
+
const tok = this.next();
|
|
661
|
+
const typ = tok.typ;
|
|
662
|
+
const val = tok.value;
|
|
663
|
+
|
|
664
|
+
if (typ === "Equals") {
|
|
665
|
+
return new Iri(OWL_NS + "sameAs");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (typ === "IriRef") {
|
|
669
|
+
return new Iri(val || "");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (typ === "Ident") {
|
|
673
|
+
const name = val || "";
|
|
674
|
+
if (name === "a") {
|
|
675
|
+
return new Iri(RDF_NS + "type");
|
|
676
|
+
} else if (name.startsWith("_:")) {
|
|
677
|
+
return new Blank(name);
|
|
678
|
+
} else if (name.includes(":")) {
|
|
679
|
+
return new Iri(this.prefixes.expandQName(name));
|
|
680
|
+
} else {
|
|
681
|
+
return new Iri(name);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (typ === "Literal") {
|
|
686
|
+
let s = val || "";
|
|
687
|
+
if (this.peek().typ === "HatHat") {
|
|
688
|
+
this.next();
|
|
689
|
+
const dtTok = this.next();
|
|
690
|
+
let dtIri;
|
|
691
|
+
if (dtTok.typ === "IriRef") {
|
|
692
|
+
dtIri = dtTok.value || "";
|
|
693
|
+
} else if (dtTok.typ === "Ident") {
|
|
694
|
+
const qn = dtTok.value || "";
|
|
695
|
+
if (qn.includes(":")) dtIri = this.prefixes.expandQName(qn);
|
|
696
|
+
else dtIri = qn;
|
|
697
|
+
} else {
|
|
698
|
+
throw new Error(`Expected datatype after ^^, got ${dtTok.toString()}`);
|
|
699
|
+
}
|
|
700
|
+
s = `${s}^^<${dtIri}>`;
|
|
701
|
+
}
|
|
702
|
+
return new Literal(s);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (typ === "Var") return new Var(val || "");
|
|
706
|
+
if (typ === "LParen") return this.parseList();
|
|
707
|
+
if (typ === "LBracket") return this.parseBlank();
|
|
708
|
+
if (typ === "LBrace") return this.parseFormula();
|
|
709
|
+
|
|
710
|
+
throw new Error(`Unexpected term token: ${tok.toString()}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
parseList() {
|
|
714
|
+
const elems = [];
|
|
715
|
+
while (this.peek().typ !== "RParen") {
|
|
716
|
+
elems.push(this.parseTerm());
|
|
717
|
+
}
|
|
718
|
+
this.next(); // consume ')'
|
|
719
|
+
return new ListTerm(elems);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
parseBlank() {
|
|
723
|
+
// [] or [ ... ] property list
|
|
724
|
+
if (this.peek().typ === "RBracket") {
|
|
725
|
+
this.next();
|
|
726
|
+
this.blankCounter += 1;
|
|
727
|
+
return new Blank(`_:b${this.blankCounter}`);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// [ predicateObjectList ]
|
|
731
|
+
this.blankCounter += 1;
|
|
732
|
+
const id = `_:b${this.blankCounter}`;
|
|
733
|
+
const subj = new Blank(id);
|
|
734
|
+
|
|
735
|
+
while (true) {
|
|
736
|
+
// Verb (can also be 'a')
|
|
737
|
+
let pred;
|
|
738
|
+
if (this.peek().typ === "Ident" && (this.peek().value || "") === "a") {
|
|
739
|
+
this.next();
|
|
740
|
+
pred = new Iri(RDF_NS + "type");
|
|
741
|
+
} else {
|
|
742
|
+
pred = this.parseTerm();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Object list: o1, o2, ...
|
|
746
|
+
const objs = [this.parseTerm()];
|
|
747
|
+
while (this.peek().typ === "Comma") {
|
|
748
|
+
this.next();
|
|
749
|
+
objs.push(this.parseTerm());
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (const o of objs) {
|
|
753
|
+
this.pendingTriples.push(new Triple(subj, pred, o));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (this.peek().typ === "Semicolon") {
|
|
757
|
+
this.next();
|
|
758
|
+
if (this.peek().typ === "RBracket") break;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (this.peek().typ === "RBracket") {
|
|
765
|
+
this.next();
|
|
766
|
+
} else {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`Expected ']' at end of blank node property list, got ${JSON.stringify(
|
|
769
|
+
this.peek()
|
|
770
|
+
)}`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return new Blank(id);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
parseFormula() {
|
|
778
|
+
const triples = [];
|
|
779
|
+
while (this.peek().typ !== "RBrace") {
|
|
780
|
+
const left = this.parseTerm();
|
|
781
|
+
if (this.peek().typ === "OpImplies") {
|
|
782
|
+
this.next();
|
|
783
|
+
const right = this.parseTerm();
|
|
784
|
+
const pred = new Iri(LOG_NS + "implies");
|
|
785
|
+
triples.push(new Triple(left, pred, right));
|
|
786
|
+
if (this.peek().typ === "Dot") this.next();
|
|
787
|
+
else if (this.peek().typ === "RBrace") {
|
|
788
|
+
// ok
|
|
789
|
+
} else {
|
|
790
|
+
throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
791
|
+
}
|
|
792
|
+
} else if (this.peek().typ === "OpImpliedBy") {
|
|
793
|
+
this.next();
|
|
794
|
+
const right = this.parseTerm();
|
|
795
|
+
const pred = new Iri(LOG_NS + "impliedBy");
|
|
796
|
+
triples.push(new Triple(left, pred, right));
|
|
797
|
+
if (this.peek().typ === "Dot") this.next();
|
|
798
|
+
else if (this.peek().typ === "RBrace") {
|
|
799
|
+
// ok
|
|
800
|
+
} else {
|
|
801
|
+
throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
triples.push(...this.parsePredicateObjectList(left));
|
|
805
|
+
if (this.peek().typ === "Dot") this.next();
|
|
806
|
+
else if (this.peek().typ === "RBrace") {
|
|
807
|
+
// ok
|
|
808
|
+
} else {
|
|
809
|
+
throw new Error(`Expected '.' or '}', got ${this.peek().toString()}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
this.next(); // consume '}'
|
|
814
|
+
return new FormulaTerm(triples);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
parsePredicateObjectList(subject) {
|
|
818
|
+
const out = [];
|
|
819
|
+
while (true) {
|
|
820
|
+
let verb;
|
|
821
|
+
if (this.peek().typ === "Ident" && (this.peek().value || "") === "a") {
|
|
822
|
+
this.next();
|
|
823
|
+
verb = new Iri(RDF_NS + "type");
|
|
824
|
+
} else {
|
|
825
|
+
verb = this.parseTerm();
|
|
826
|
+
}
|
|
827
|
+
const objects = this.parseObjectList();
|
|
828
|
+
for (const o of objects) out.push(new Triple(subject, verb, o));
|
|
829
|
+
|
|
830
|
+
if (this.peek().typ === "Semicolon") {
|
|
831
|
+
this.next();
|
|
832
|
+
if (this.peek().typ === "Dot") break;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (this.pendingTriples.length > 0) {
|
|
839
|
+
out.push(...this.pendingTriples);
|
|
840
|
+
this.pendingTriples = [];
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
parseObjectList() {
|
|
847
|
+
const objs = [this.parseTerm()];
|
|
848
|
+
while (this.peek().typ === "Comma") {
|
|
849
|
+
this.next();
|
|
850
|
+
objs.push(this.parseTerm());
|
|
851
|
+
}
|
|
852
|
+
return objs;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
makeRule(left, right, isForward) {
|
|
856
|
+
let premiseTerm, conclTerm;
|
|
857
|
+
|
|
858
|
+
if (isForward) {
|
|
859
|
+
premiseTerm = left;
|
|
860
|
+
conclTerm = right;
|
|
861
|
+
} else {
|
|
862
|
+
premiseTerm = right;
|
|
863
|
+
conclTerm = left;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
let isFuse = false;
|
|
867
|
+
if (isForward) {
|
|
868
|
+
if (conclTerm instanceof Literal && conclTerm.value === "false") {
|
|
869
|
+
isFuse = true;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
let rawPremise;
|
|
874
|
+
if (premiseTerm instanceof FormulaTerm) {
|
|
875
|
+
rawPremise = premiseTerm.triples;
|
|
876
|
+
} else if (premiseTerm instanceof Literal && premiseTerm.value === "true") {
|
|
877
|
+
rawPremise = [];
|
|
878
|
+
} else {
|
|
879
|
+
rawPremise = [];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
let rawConclusion;
|
|
883
|
+
if (conclTerm instanceof FormulaTerm) {
|
|
884
|
+
rawConclusion = conclTerm.triples;
|
|
885
|
+
} else if (conclTerm instanceof Literal && conclTerm.value === "false") {
|
|
886
|
+
rawConclusion = [];
|
|
887
|
+
} else {
|
|
888
|
+
rawConclusion = [];
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Blank nodes that occur explicitly in the head (conclusion)
|
|
892
|
+
const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
|
|
893
|
+
|
|
894
|
+
const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
895
|
+
|
|
896
|
+
// Reorder constraints for *forward* rules.
|
|
897
|
+
const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
|
|
898
|
+
|
|
899
|
+
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// ============================================================================
|
|
904
|
+
// Blank-node lifting and Skolemization
|
|
905
|
+
// ============================================================================
|
|
906
|
+
|
|
907
|
+
function liftBlankRuleVars(premise, conclusion) {
|
|
908
|
+
function convertTerm(t, mapping, counter) {
|
|
909
|
+
if (t instanceof Blank) {
|
|
910
|
+
const label = t.label;
|
|
911
|
+
if (!mapping.hasOwnProperty(label)) {
|
|
912
|
+
counter[0] += 1;
|
|
913
|
+
mapping[label] = `_b${counter[0]}`;
|
|
914
|
+
}
|
|
915
|
+
return new Var(mapping[label]);
|
|
916
|
+
}
|
|
917
|
+
if (t instanceof ListTerm) {
|
|
918
|
+
return new ListTerm(t.elems.map(e => convertTerm(e, mapping, counter)));
|
|
919
|
+
}
|
|
920
|
+
if (t instanceof OpenListTerm) {
|
|
921
|
+
return new OpenListTerm(
|
|
922
|
+
t.prefix.map(e => convertTerm(e, mapping, counter)),
|
|
923
|
+
t.tailVar
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
if (t instanceof FormulaTerm) {
|
|
927
|
+
const triples = t.triples.map(tr =>
|
|
928
|
+
new Triple(
|
|
929
|
+
convertTerm(tr.s, mapping, counter),
|
|
930
|
+
convertTerm(tr.p, mapping, counter),
|
|
931
|
+
convertTerm(tr.o, mapping, counter)
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
return new FormulaTerm(triples);
|
|
935
|
+
}
|
|
936
|
+
return t;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function convertTriple(tr, mapping, counter) {
|
|
940
|
+
return new Triple(
|
|
941
|
+
convertTerm(tr.s, mapping, counter),
|
|
942
|
+
convertTerm(tr.p, mapping, counter),
|
|
943
|
+
convertTerm(tr.o, mapping, counter)
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const mapping = {};
|
|
948
|
+
const counter = [0];
|
|
949
|
+
const newPremise = premise.map(tr => convertTriple(tr, mapping, counter));
|
|
950
|
+
return [newPremise, conclusion];
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter) {
|
|
954
|
+
if (t instanceof Blank) {
|
|
955
|
+
const label = t.label;
|
|
956
|
+
// Only skolemize blanks that occur explicitly in the rule head
|
|
957
|
+
if (!headBlankLabels || !headBlankLabels.has(label)) {
|
|
958
|
+
return t; // this is a data blank (e.g. bound via ?X), keep it
|
|
959
|
+
}
|
|
960
|
+
if (!mapping.hasOwnProperty(label)) {
|
|
961
|
+
const idx = skCounter[0];
|
|
962
|
+
skCounter[0] += 1;
|
|
963
|
+
mapping[label] = `_:sk_${idx}`;
|
|
964
|
+
}
|
|
965
|
+
return new Blank(mapping[label]);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (t instanceof ListTerm) {
|
|
969
|
+
return new ListTerm(
|
|
970
|
+
t.elems.map(e => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter))
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (t instanceof OpenListTerm) {
|
|
975
|
+
return new OpenListTerm(
|
|
976
|
+
t.prefix.map(e => skolemizeTermForHeadBlanks(e, headBlankLabels, mapping, skCounter)),
|
|
977
|
+
t.tailVar
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (t instanceof FormulaTerm) {
|
|
982
|
+
return new FormulaTerm(
|
|
983
|
+
t.triples.map(tr =>
|
|
984
|
+
skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter)
|
|
985
|
+
)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return t;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter) {
|
|
993
|
+
return new Triple(
|
|
994
|
+
skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter),
|
|
995
|
+
skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter),
|
|
996
|
+
skolemizeTermForHeadBlanks(tr.o, headBlankLabels, mapping, skCounter)
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ============================================================================
|
|
1001
|
+
// Alpha equivalence helpers
|
|
1002
|
+
// ============================================================================
|
|
1003
|
+
|
|
1004
|
+
function termsEqual(a, b) {
|
|
1005
|
+
if (a === b) return true;
|
|
1006
|
+
if (!a || !b) return false;
|
|
1007
|
+
if (a.constructor !== b.constructor) return false;
|
|
1008
|
+
if (a instanceof Iri) return a.value === b.value;
|
|
1009
|
+
if (a instanceof Literal) return a.value === b.value;
|
|
1010
|
+
if (a instanceof Var) return a.name === b.name;
|
|
1011
|
+
if (a instanceof Blank) return a.label === b.label;
|
|
1012
|
+
if (a instanceof ListTerm) {
|
|
1013
|
+
if (a.elems.length !== b.elems.length) return false;
|
|
1014
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
1015
|
+
if (!termsEqual(a.elems[i], b.elems[i])) return false;
|
|
1016
|
+
}
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
if (a instanceof OpenListTerm) {
|
|
1020
|
+
if (a.tailVar !== b.tailVar) return false;
|
|
1021
|
+
if (a.prefix.length !== b.prefix.length) return false;
|
|
1022
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
1023
|
+
if (!termsEqual(a.prefix[i], b.prefix[i])) return false;
|
|
1024
|
+
}
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
if (a instanceof FormulaTerm) {
|
|
1028
|
+
return triplesListEqual(a.triples, b.triples);
|
|
1029
|
+
}
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function triplesEqual(a, b) {
|
|
1034
|
+
return (
|
|
1035
|
+
termsEqual(a.s, b.s) && termsEqual(a.p, b.p) && termsEqual(a.o, b.o)
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function triplesListEqual(xs, ys) {
|
|
1040
|
+
if (xs.length !== ys.length) return false;
|
|
1041
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1042
|
+
if (!triplesEqual(xs[i], ys[i])) return false;
|
|
1043
|
+
}
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function alphaEqTerm(a, b, bmap) {
|
|
1048
|
+
if (a instanceof Blank && b instanceof Blank) {
|
|
1049
|
+
const x = a.label;
|
|
1050
|
+
const y = b.label;
|
|
1051
|
+
if (bmap.hasOwnProperty(x)) {
|
|
1052
|
+
return bmap[x] === y;
|
|
1053
|
+
} else {
|
|
1054
|
+
bmap[x] = y;
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (a instanceof Iri && b instanceof Iri) return a.value === b.value;
|
|
1059
|
+
if (a instanceof Literal && b instanceof Literal) return a.value === b.value;
|
|
1060
|
+
if (a instanceof Var && b instanceof Var) return a.name === b.name;
|
|
1061
|
+
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
1062
|
+
if (a.elems.length !== b.elems.length) return false;
|
|
1063
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
1064
|
+
if (!alphaEqTerm(a.elems[i], b.elems[i], bmap)) return false;
|
|
1065
|
+
}
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
1069
|
+
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
|
|
1070
|
+
return false;
|
|
1071
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
1072
|
+
if (!alphaEqTerm(a.prefix[i], b.prefix[i], bmap)) return false;
|
|
1073
|
+
}
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
if (a instanceof FormulaTerm && b instanceof FormulaTerm) {
|
|
1077
|
+
// formulas are treated as opaque here: exact equality
|
|
1078
|
+
return triplesListEqual(a.triples, b.triples);
|
|
1079
|
+
}
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function alphaEqTriple(a, b) {
|
|
1084
|
+
const bmap = {};
|
|
1085
|
+
return (
|
|
1086
|
+
alphaEqTerm(a.s, b.s, bmap) &&
|
|
1087
|
+
alphaEqTerm(a.p, b.p, bmap) &&
|
|
1088
|
+
alphaEqTerm(a.o, b.o, bmap)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function hasAlphaEquiv(triples, tr) {
|
|
1093
|
+
return triples.some(t => alphaEqTriple(t, tr));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ============================================================================
|
|
1097
|
+
// Indexes (facts + backward rules)
|
|
1098
|
+
// ============================================================================
|
|
1099
|
+
//
|
|
1100
|
+
// Facts:
|
|
1101
|
+
// - __byPred: Map<predicateIRI, Triple[]>
|
|
1102
|
+
// - __byPO: Map<predicateIRI, Map<objectKey, Triple[]>>
|
|
1103
|
+
// - __keySet: Set<"S\tP\tO"> for IRI/Literal-only triples (fast dup check)
|
|
1104
|
+
//
|
|
1105
|
+
// Backward rules:
|
|
1106
|
+
// - __byHeadPred: Map<headPredicateIRI, Rule[]>
|
|
1107
|
+
// - __wildHeadPred: Rule[] (non-IRI head predicate)
|
|
1108
|
+
|
|
1109
|
+
function termFastKey(t) {
|
|
1110
|
+
if (t instanceof Iri) return "I:" + t.value;
|
|
1111
|
+
if (t instanceof Literal) return "L:" + t.value;
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function tripleFastKey(tr) {
|
|
1116
|
+
const ks = termFastKey(tr.s);
|
|
1117
|
+
const kp = termFastKey(tr.p);
|
|
1118
|
+
const ko = termFastKey(tr.o);
|
|
1119
|
+
if (ks === null || kp === null || ko === null) return null;
|
|
1120
|
+
return ks + "\t" + kp + "\t" + ko;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function ensureFactIndexes(facts) {
|
|
1124
|
+
if (facts.__byPred && facts.__byPO && facts.__keySet) return;
|
|
1125
|
+
|
|
1126
|
+
Object.defineProperty(facts, "__byPred", { value: new Map(), enumerable: false, writable: true });
|
|
1127
|
+
Object.defineProperty(facts, "__byPO", { value: new Map(), enumerable: false, writable: true });
|
|
1128
|
+
Object.defineProperty(facts, "__keySet", { value: new Set(), enumerable: false, writable: true });
|
|
1129
|
+
|
|
1130
|
+
for (const f of facts) indexFact(facts, f);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function indexFact(facts, tr) {
|
|
1134
|
+
if (tr.p instanceof Iri) {
|
|
1135
|
+
const pk = tr.p.value;
|
|
1136
|
+
|
|
1137
|
+
let pb = facts.__byPred.get(pk);
|
|
1138
|
+
if (!pb) { pb = []; facts.__byPred.set(pk, pb); }
|
|
1139
|
+
pb.push(tr);
|
|
1140
|
+
|
|
1141
|
+
const ok = termFastKey(tr.o);
|
|
1142
|
+
if (ok !== null) {
|
|
1143
|
+
let po = facts.__byPO.get(pk);
|
|
1144
|
+
if (!po) { po = new Map(); facts.__byPO.set(pk, po); }
|
|
1145
|
+
let pob = po.get(ok);
|
|
1146
|
+
if (!pob) { pob = []; po.set(ok, pob); }
|
|
1147
|
+
pob.push(tr);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const key = tripleFastKey(tr);
|
|
1152
|
+
if (key !== null) facts.__keySet.add(key);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function candidateFacts(facts, goal) {
|
|
1156
|
+
ensureFactIndexes(facts);
|
|
1157
|
+
|
|
1158
|
+
if (goal.p instanceof Iri) {
|
|
1159
|
+
const pk = goal.p.value;
|
|
1160
|
+
|
|
1161
|
+
const ok = termFastKey(goal.o);
|
|
1162
|
+
if (ok !== null) {
|
|
1163
|
+
const po = facts.__byPO.get(pk);
|
|
1164
|
+
if (po) {
|
|
1165
|
+
const pob = po.get(ok);
|
|
1166
|
+
if (pob) return pob;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return facts.__byPred.get(pk) || [];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return facts;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function hasFactIndexed(facts, tr) {
|
|
1177
|
+
ensureFactIndexes(facts);
|
|
1178
|
+
|
|
1179
|
+
const key = tripleFastKey(tr);
|
|
1180
|
+
if (key !== null) return facts.__keySet.has(key);
|
|
1181
|
+
|
|
1182
|
+
if (tr.p instanceof Iri) {
|
|
1183
|
+
const pk = tr.p.value;
|
|
1184
|
+
|
|
1185
|
+
const ok = termFastKey(tr.o);
|
|
1186
|
+
if (ok !== null) {
|
|
1187
|
+
const po = facts.__byPO.get(pk);
|
|
1188
|
+
if (po) {
|
|
1189
|
+
const pob = po.get(ok) || [];
|
|
1190
|
+
return pob.some(t => alphaEqTriple(t, tr));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const pb = facts.__byPred.get(pk) || [];
|
|
1195
|
+
return pb.some(t => alphaEqTriple(t, tr));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
return hasAlphaEquiv(facts, tr);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function pushFactIndexed(facts, tr) {
|
|
1202
|
+
ensureFactIndexes(facts);
|
|
1203
|
+
facts.push(tr);
|
|
1204
|
+
indexFact(facts, tr);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function ensureBackRuleIndexes(backRules) {
|
|
1208
|
+
if (backRules.__byHeadPred && backRules.__wildHeadPred) return;
|
|
1209
|
+
|
|
1210
|
+
Object.defineProperty(backRules, "__byHeadPred", { value: new Map(), enumerable: false, writable: true });
|
|
1211
|
+
Object.defineProperty(backRules, "__wildHeadPred", { value: [], enumerable: false, writable: true });
|
|
1212
|
+
|
|
1213
|
+
for (const r of backRules) indexBackRule(backRules, r);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function indexBackRule(backRules, r) {
|
|
1217
|
+
if (!r || !r.conclusion || r.conclusion.length !== 1) return;
|
|
1218
|
+
const head = r.conclusion[0];
|
|
1219
|
+
if (head && head.p instanceof Iri) {
|
|
1220
|
+
const k = head.p.value;
|
|
1221
|
+
let bucket = backRules.__byHeadPred.get(k);
|
|
1222
|
+
if (!bucket) { bucket = []; backRules.__byHeadPred.set(k, bucket); }
|
|
1223
|
+
bucket.push(r);
|
|
1224
|
+
} else {
|
|
1225
|
+
backRules.__wildHeadPred.push(r);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// ============================================================================
|
|
1230
|
+
// Special predicate helpers
|
|
1231
|
+
// ============================================================================
|
|
1232
|
+
|
|
1233
|
+
function isRdfTypePred(p) {
|
|
1234
|
+
return p instanceof Iri && p.value === RDF_NS + "type";
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function isOwlSameAsPred(t) {
|
|
1238
|
+
return t instanceof Iri && t.value === (OWL_NS + "sameAs");
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function isLogImplies(p) {
|
|
1242
|
+
return p instanceof Iri && p.value === LOG_NS + "implies";
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function isLogImpliedBy(p) {
|
|
1246
|
+
return p instanceof Iri && p.value === LOG_NS + "impliedBy";
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// ============================================================================
|
|
1250
|
+
// Constraint / "test" builtins
|
|
1251
|
+
// ============================================================================
|
|
1252
|
+
|
|
1253
|
+
function isConstraintBuiltin(tr) {
|
|
1254
|
+
if (!(tr.p instanceof Iri)) return false;
|
|
1255
|
+
const v = tr.p.value;
|
|
1256
|
+
|
|
1257
|
+
// math: numeric comparisons (no new bindings, just tests)
|
|
1258
|
+
if (
|
|
1259
|
+
v === MATH_NS + "equalTo" ||
|
|
1260
|
+
v === MATH_NS + "greaterThan" ||
|
|
1261
|
+
v === MATH_NS + "lessThan" ||
|
|
1262
|
+
v === MATH_NS + "notEqualTo" ||
|
|
1263
|
+
v === MATH_NS + "notGreaterThan" ||
|
|
1264
|
+
v === MATH_NS + "notLessThan"
|
|
1265
|
+
) {
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// list: membership test with no bindings
|
|
1270
|
+
if (v === LIST_NS + "notMember") {
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// log: tests that are purely constraints (no new bindings)
|
|
1275
|
+
if (
|
|
1276
|
+
v === LOG_NS + "forAllIn" ||
|
|
1277
|
+
v === LOG_NS + "notEqualTo" ||
|
|
1278
|
+
v === LOG_NS + "notIncludes"
|
|
1279
|
+
) {
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// string: relational / membership style tests (no bindings)
|
|
1284
|
+
if (
|
|
1285
|
+
v === STRING_NS + "contains" ||
|
|
1286
|
+
v === STRING_NS + "containsIgnoringCase" ||
|
|
1287
|
+
v === STRING_NS + "endsWith" ||
|
|
1288
|
+
v === STRING_NS + "equalIgnoringCase" ||
|
|
1289
|
+
v === STRING_NS + "greaterThan" ||
|
|
1290
|
+
v === STRING_NS + "lessThan" ||
|
|
1291
|
+
v === STRING_NS + "matches" ||
|
|
1292
|
+
v === STRING_NS + "notEqualIgnoringCase" ||
|
|
1293
|
+
v === STRING_NS + "notGreaterThan" ||
|
|
1294
|
+
v === STRING_NS + "notLessThan" ||
|
|
1295
|
+
v === STRING_NS + "notMatches" ||
|
|
1296
|
+
v === STRING_NS + "startsWith"
|
|
1297
|
+
) {
|
|
1298
|
+
return true;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Move constraint builtins to the end of the rule premise.
|
|
1306
|
+
* This is a simple "delaying" strategy similar in spirit to Prolog's when/2:
|
|
1307
|
+
* - normal goals first (can bind variables),
|
|
1308
|
+
* - pure test / constraint builtins last (checked once bindings are in place).
|
|
1309
|
+
*/
|
|
1310
|
+
function reorderPremiseForConstraints(premise) {
|
|
1311
|
+
if (!premise || premise.length === 0) return premise;
|
|
1312
|
+
|
|
1313
|
+
const normal = [];
|
|
1314
|
+
const delayed = [];
|
|
1315
|
+
|
|
1316
|
+
for (const tr of premise) {
|
|
1317
|
+
if (isConstraintBuiltin(tr)) delayed.push(tr);
|
|
1318
|
+
else normal.push(tr);
|
|
1319
|
+
}
|
|
1320
|
+
return normal.concat(delayed);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// ============================================================================
|
|
1324
|
+
// Unification + substitution
|
|
1325
|
+
// ============================================================================
|
|
1326
|
+
|
|
1327
|
+
function containsVarTerm(t, v) {
|
|
1328
|
+
if (t instanceof Var) return t.name === v;
|
|
1329
|
+
if (t instanceof ListTerm) return t.elems.some(e => containsVarTerm(e, v));
|
|
1330
|
+
if (t instanceof OpenListTerm)
|
|
1331
|
+
return (
|
|
1332
|
+
t.prefix.some(e => containsVarTerm(e, v)) || t.tailVar === v
|
|
1333
|
+
);
|
|
1334
|
+
if (t instanceof FormulaTerm)
|
|
1335
|
+
return t.triples.some(
|
|
1336
|
+
tr =>
|
|
1337
|
+
containsVarTerm(tr.s, v) ||
|
|
1338
|
+
containsVarTerm(tr.p, v) ||
|
|
1339
|
+
containsVarTerm(tr.o, v)
|
|
1340
|
+
);
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function isGroundTerm(t) {
|
|
1345
|
+
if (t instanceof Var) return false;
|
|
1346
|
+
if (t instanceof ListTerm) return t.elems.every(e => isGroundTerm(e));
|
|
1347
|
+
if (t instanceof OpenListTerm) return false;
|
|
1348
|
+
if (t instanceof FormulaTerm)
|
|
1349
|
+
return t.triples.every(tr => isGroundTriple(tr));
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function isGroundTriple(tr) {
|
|
1354
|
+
return isGroundTerm(tr.s) && isGroundTerm(tr.p) && isGroundTerm(tr.o);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Canonical JSON-ish encoding for use as a Skolem cache key.
|
|
1358
|
+
// We only *call* this on ground terms in log:skolem, but it is
|
|
1359
|
+
// robust to seeing vars/open lists anyway.
|
|
1360
|
+
function skolemKeyFromTerm(t) {
|
|
1361
|
+
function enc(u) {
|
|
1362
|
+
if (u instanceof Iri) return ["I", u.value];
|
|
1363
|
+
if (u instanceof Literal) return ["L", u.value];
|
|
1364
|
+
if (u instanceof Blank) return ["B", u.label];
|
|
1365
|
+
if (u instanceof Var) return ["V", u.name];
|
|
1366
|
+
if (u instanceof ListTerm) return ["List", u.elems.map(enc)];
|
|
1367
|
+
if (u instanceof OpenListTerm)
|
|
1368
|
+
return ["OpenList", u.prefix.map(enc), u.tailVar];
|
|
1369
|
+
if (u instanceof FormulaTerm)
|
|
1370
|
+
return [
|
|
1371
|
+
"Formula",
|
|
1372
|
+
u.triples.map(tr => [
|
|
1373
|
+
enc(tr.s),
|
|
1374
|
+
enc(tr.p),
|
|
1375
|
+
enc(tr.o)
|
|
1376
|
+
])
|
|
1377
|
+
];
|
|
1378
|
+
return ["Other", String(u)];
|
|
1379
|
+
}
|
|
1380
|
+
return JSON.stringify(enc(t));
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function applySubstTerm(t, s) {
|
|
1384
|
+
// Common case: variable
|
|
1385
|
+
if (t instanceof Var) {
|
|
1386
|
+
// Fast path: unbound variable → no change
|
|
1387
|
+
const first = s[t.name];
|
|
1388
|
+
if (first === undefined) {
|
|
1389
|
+
return t;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Follow chains X -> Y -> ... until we hit a non-var or a cycle.
|
|
1393
|
+
let cur = first;
|
|
1394
|
+
const seen = new Set([t.name]);
|
|
1395
|
+
while (cur instanceof Var) {
|
|
1396
|
+
const name = cur.name;
|
|
1397
|
+
if (seen.has(name)) break; // cycle
|
|
1398
|
+
seen.add(name);
|
|
1399
|
+
const nxt = s[name];
|
|
1400
|
+
if (!nxt) break;
|
|
1401
|
+
cur = nxt;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (cur instanceof Var) {
|
|
1405
|
+
// Still a var: keep it as is (no need to clone)
|
|
1406
|
+
return cur;
|
|
1407
|
+
}
|
|
1408
|
+
// Bound to a non-var term: apply substitution recursively in case it
|
|
1409
|
+
// contains variables inside.
|
|
1410
|
+
return applySubstTerm(cur, s);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Non-variable terms
|
|
1414
|
+
if (t instanceof ListTerm) {
|
|
1415
|
+
return new ListTerm(t.elems.map(e => applySubstTerm(e, s)));
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (t instanceof OpenListTerm) {
|
|
1419
|
+
const newPrefix = t.prefix.map(e => applySubstTerm(e, s));
|
|
1420
|
+
const tailTerm = s[t.tailVar];
|
|
1421
|
+
if (tailTerm !== undefined) {
|
|
1422
|
+
const tailApplied = applySubstTerm(tailTerm, s);
|
|
1423
|
+
if (tailApplied instanceof ListTerm) {
|
|
1424
|
+
return new ListTerm(newPrefix.concat(tailApplied.elems));
|
|
1425
|
+
} else if (tailApplied instanceof OpenListTerm) {
|
|
1426
|
+
return new OpenListTerm(
|
|
1427
|
+
newPrefix.concat(tailApplied.prefix),
|
|
1428
|
+
tailApplied.tailVar
|
|
1429
|
+
);
|
|
1430
|
+
} else {
|
|
1431
|
+
return new OpenListTerm(newPrefix, t.tailVar);
|
|
1432
|
+
}
|
|
1433
|
+
} else {
|
|
1434
|
+
return new OpenListTerm(newPrefix, t.tailVar);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
if (t instanceof FormulaTerm) {
|
|
1439
|
+
return new FormulaTerm(t.triples.map(tr => applySubstTriple(tr, s)));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
return t;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function applySubstTriple(tr, s) {
|
|
1446
|
+
return new Triple(
|
|
1447
|
+
applySubstTerm(tr.s, s),
|
|
1448
|
+
applySubstTerm(tr.p, s),
|
|
1449
|
+
applySubstTerm(tr.o, s)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
1454
|
+
if (ys.length < prefix.length) return null;
|
|
1455
|
+
let s2 = { ...subst };
|
|
1456
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
1457
|
+
s2 = unifyTerm(prefix[i], ys[i], s2);
|
|
1458
|
+
if (s2 === null) return null;
|
|
1459
|
+
}
|
|
1460
|
+
const rest = new ListTerm(ys.slice(prefix.length));
|
|
1461
|
+
s2 = unifyTerm(new Var(tailv), rest, s2);
|
|
1462
|
+
if (s2 === null) return null;
|
|
1463
|
+
return s2;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function unifyTerm(a, b, subst) {
|
|
1467
|
+
a = applySubstTerm(a, subst);
|
|
1468
|
+
b = applySubstTerm(b, subst);
|
|
1469
|
+
|
|
1470
|
+
// Variable binding
|
|
1471
|
+
if (a instanceof Var) {
|
|
1472
|
+
const v = a.name;
|
|
1473
|
+
const t = b;
|
|
1474
|
+
if (t instanceof Var && t.name === v) return { ...subst };
|
|
1475
|
+
if (containsVarTerm(t, v)) return null;
|
|
1476
|
+
const s2 = { ...subst };
|
|
1477
|
+
s2[v] = t;
|
|
1478
|
+
return s2;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (b instanceof Var) {
|
|
1482
|
+
return unifyTerm(b, a, subst);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Exact matches
|
|
1486
|
+
if (a instanceof Iri && b instanceof Iri && a.value === b.value)
|
|
1487
|
+
return { ...subst };
|
|
1488
|
+
if (a instanceof Literal && b instanceof Literal && a.value === b.value)
|
|
1489
|
+
return { ...subst };
|
|
1490
|
+
if (a instanceof Blank && b instanceof Blank && a.label === b.label)
|
|
1491
|
+
return { ...subst };
|
|
1492
|
+
|
|
1493
|
+
// Open list vs concrete list
|
|
1494
|
+
if (a instanceof OpenListTerm && b instanceof ListTerm) {
|
|
1495
|
+
return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
|
|
1496
|
+
}
|
|
1497
|
+
if (a instanceof ListTerm && b instanceof OpenListTerm) {
|
|
1498
|
+
return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Open list vs open list (same tail var)
|
|
1502
|
+
if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
|
|
1503
|
+
if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length)
|
|
1504
|
+
return null;
|
|
1505
|
+
let s2 = { ...subst };
|
|
1506
|
+
for (let i = 0; i < a.prefix.length; i++) {
|
|
1507
|
+
s2 = unifyTerm(a.prefix[i], b.prefix[i], s2);
|
|
1508
|
+
if (s2 === null) return null;
|
|
1509
|
+
}
|
|
1510
|
+
return s2;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// List terms
|
|
1514
|
+
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
1515
|
+
if (a.elems.length !== b.elems.length) return null;
|
|
1516
|
+
let s2 = { ...subst };
|
|
1517
|
+
for (let i = 0; i < a.elems.length; i++) {
|
|
1518
|
+
s2 = unifyTerm(a.elems[i], b.elems[i], s2);
|
|
1519
|
+
if (s2 === null) return null;
|
|
1520
|
+
}
|
|
1521
|
+
return s2;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Formulas are treated as opaque unless exactly equal
|
|
1525
|
+
if (a instanceof FormulaTerm && b instanceof FormulaTerm) {
|
|
1526
|
+
if (triplesListEqual(a.triples, b.triples)) return { ...subst };
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function unifyTriple(pat, fact, subst) {
|
|
1533
|
+
// Predicates are usually the cheapest and most selective
|
|
1534
|
+
const s1 = unifyTerm(pat.p, fact.p, subst);
|
|
1535
|
+
if (s1 === null) return null;
|
|
1536
|
+
|
|
1537
|
+
const s2 = unifyTerm(pat.s, fact.s, s1);
|
|
1538
|
+
if (s2 === null) return null;
|
|
1539
|
+
|
|
1540
|
+
const s3 = unifyTerm(pat.o, fact.o, s2);
|
|
1541
|
+
return s3;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function composeSubst(outer, delta) {
|
|
1545
|
+
if (!delta || Object.keys(delta).length === 0) {
|
|
1546
|
+
return { ...outer };
|
|
1547
|
+
}
|
|
1548
|
+
const out = { ...outer };
|
|
1549
|
+
for (const [k, v] of Object.entries(delta)) {
|
|
1550
|
+
if (out.hasOwnProperty(k)) {
|
|
1551
|
+
if (!termsEqual(out[k], v)) return null;
|
|
1552
|
+
} else {
|
|
1553
|
+
out[k] = v;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return out;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// ============================================================================
|
|
1560
|
+
// BUILTINS
|
|
1561
|
+
// ============================================================================
|
|
1562
|
+
|
|
1563
|
+
function literalParts(lit) {
|
|
1564
|
+
const idx = lit.indexOf("^^");
|
|
1565
|
+
if (idx >= 0) {
|
|
1566
|
+
let lex = lit.slice(0, idx);
|
|
1567
|
+
let dt = lit.slice(idx + 2).trim();
|
|
1568
|
+
if (dt.startsWith("<") && dt.endsWith(">")) {
|
|
1569
|
+
dt = dt.slice(1, -1);
|
|
1570
|
+
}
|
|
1571
|
+
return [lex, dt];
|
|
1572
|
+
}
|
|
1573
|
+
return [lit, null];
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function stripQuotes(lex) {
|
|
1577
|
+
if (lex.length >= 2 && lex[0] === '"' && lex[lex.length - 1] === '"') {
|
|
1578
|
+
return lex.slice(1, -1);
|
|
1579
|
+
}
|
|
1580
|
+
return lex;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function termToJsString(t) {
|
|
1584
|
+
// Accept any Literal and interpret its lexical form as a JS string.
|
|
1585
|
+
if (!(t instanceof Literal)) return null;
|
|
1586
|
+
const [lex, _dt] = literalParts(t.value);
|
|
1587
|
+
return stripQuotes(lex);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function makeStringLiteral(str) {
|
|
1591
|
+
// JSON.stringify gives us a valid N3/Turtle-style quoted string
|
|
1592
|
+
// (with proper escaping for quotes, backslashes, newlines, …).
|
|
1593
|
+
return new Literal(JSON.stringify(str));
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Tiny subset of sprintf: supports only %s and %%.
|
|
1597
|
+
// Good enough for most N3 string:format use cases that just splice strings.
|
|
1598
|
+
function simpleStringFormat(fmt, args) {
|
|
1599
|
+
let out = "";
|
|
1600
|
+
let argIndex = 0;
|
|
1601
|
+
|
|
1602
|
+
for (let i = 0; i < fmt.length; i++) {
|
|
1603
|
+
const ch = fmt[i];
|
|
1604
|
+
if (ch === "%" && i + 1 < fmt.length) {
|
|
1605
|
+
const spec = fmt[i + 1];
|
|
1606
|
+
|
|
1607
|
+
if (spec === "s") {
|
|
1608
|
+
const arg = argIndex < args.length ? args[argIndex++] : "";
|
|
1609
|
+
out += arg;
|
|
1610
|
+
i++;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
if (spec === "%") {
|
|
1615
|
+
out += "%";
|
|
1616
|
+
i++;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Unsupported specifier (like %d, %f, …) ⇒ fail the builtin.
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
1623
|
+
out += ch;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return out;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function parseNum(t) {
|
|
1630
|
+
// Parse as JS Number (for floats, dates-as-seconds, etc.)
|
|
1631
|
+
if (!(t instanceof Literal)) return null;
|
|
1632
|
+
let s = t.value;
|
|
1633
|
+
let [lex, _dt] = literalParts(s);
|
|
1634
|
+
const val = stripQuotes(lex);
|
|
1635
|
+
const n = Number(val);
|
|
1636
|
+
if (!Number.isNaN(n)) return n;
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function parseIntLiteral(t) {
|
|
1641
|
+
// Parse as BigInt if the lexical form is an integer
|
|
1642
|
+
if (!(t instanceof Literal)) return null;
|
|
1643
|
+
let s = t.value;
|
|
1644
|
+
let [lex, _dt] = literalParts(s);
|
|
1645
|
+
const val = stripQuotes(lex);
|
|
1646
|
+
if (!/^[+-]?\d+$/.test(val)) return null;
|
|
1647
|
+
try {
|
|
1648
|
+
return BigInt(val);
|
|
1649
|
+
} catch (e) {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function parseNumberLiteral(t) {
|
|
1655
|
+
// Prefer BigInt for integers, fall back to Number for non-integers
|
|
1656
|
+
const bi = parseIntLiteral(t);
|
|
1657
|
+
if (bi !== null) return bi;
|
|
1658
|
+
const n = parseNum(t);
|
|
1659
|
+
if (n !== null) return n;
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function formatNum(n) {
|
|
1664
|
+
return String(n);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function parseXsdDateTerm(t) {
|
|
1668
|
+
if (!(t instanceof Literal)) return null;
|
|
1669
|
+
const s = t.value;
|
|
1670
|
+
let [lex, dt] = literalParts(s);
|
|
1671
|
+
const val = stripQuotes(lex);
|
|
1672
|
+
if (dt === XSD_NS + "date" || val.length === 10) {
|
|
1673
|
+
const d = new Date(val + "T00:00:00Z");
|
|
1674
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
1675
|
+
return d;
|
|
1676
|
+
}
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function parseXsdDatetimeTerm(t) {
|
|
1681
|
+
if (!(t instanceof Literal)) return null;
|
|
1682
|
+
const s = t.value;
|
|
1683
|
+
let [lex, dt] = literalParts(s);
|
|
1684
|
+
const val = stripQuotes(lex);
|
|
1685
|
+
if (dt === XSD_NS + "dateTime" || val.includes("T")) {
|
|
1686
|
+
const d = new Date(val);
|
|
1687
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
1688
|
+
return d; // Date in local/UTC, we only use timestamp
|
|
1689
|
+
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function parseDatetimeLike(t) {
|
|
1694
|
+
const d = parseXsdDateTerm(t);
|
|
1695
|
+
if (d !== null) return d;
|
|
1696
|
+
return parseXsdDatetimeTerm(t);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function parseIso8601DurationToSeconds(s) {
|
|
1700
|
+
if (!s) return null;
|
|
1701
|
+
if (s[0] !== "P") return null;
|
|
1702
|
+
const it = s.slice(1);
|
|
1703
|
+
let num = "";
|
|
1704
|
+
let inTime = false;
|
|
1705
|
+
let years = 0,
|
|
1706
|
+
months = 0,
|
|
1707
|
+
weeks = 0,
|
|
1708
|
+
days = 0,
|
|
1709
|
+
hours = 0,
|
|
1710
|
+
minutes = 0,
|
|
1711
|
+
seconds = 0;
|
|
1712
|
+
|
|
1713
|
+
for (const c of it) {
|
|
1714
|
+
if (c === "T") {
|
|
1715
|
+
inTime = true;
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
if (/[0-9.]/.test(c)) {
|
|
1719
|
+
num += c;
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
if (!num) return null;
|
|
1723
|
+
const val = Number(num);
|
|
1724
|
+
if (Number.isNaN(val)) return null;
|
|
1725
|
+
num = "";
|
|
1726
|
+
if (!inTime && c === "Y") years += val;
|
|
1727
|
+
else if (!inTime && c === "M") months += val;
|
|
1728
|
+
else if (!inTime && c === "W") weeks += val;
|
|
1729
|
+
else if (!inTime && c === "D") days += val;
|
|
1730
|
+
else if (inTime && c === "H") hours += val;
|
|
1731
|
+
else if (inTime && c === "M") minutes += val;
|
|
1732
|
+
else if (inTime && c === "S") seconds += val;
|
|
1733
|
+
else return null;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const totalDays =
|
|
1737
|
+
years * 365.2425 +
|
|
1738
|
+
months * 30.436875 +
|
|
1739
|
+
weeks * 7.0 +
|
|
1740
|
+
days +
|
|
1741
|
+
hours / 24.0 +
|
|
1742
|
+
minutes / (24.0 * 60.0) +
|
|
1743
|
+
seconds / (24.0 * 3600.0);
|
|
1744
|
+
|
|
1745
|
+
return totalDays * 86400.0;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function parseNumericForCompareTerm(t) {
|
|
1749
|
+
// Try integer BigInt first
|
|
1750
|
+
if (t instanceof Literal) {
|
|
1751
|
+
let [lex, dt] = literalParts(t.value);
|
|
1752
|
+
const val = stripQuotes(lex);
|
|
1753
|
+
if (/^[+-]?\d+$/.test(val)) {
|
|
1754
|
+
try {
|
|
1755
|
+
return { kind: "bigint", value: BigInt(val) };
|
|
1756
|
+
} catch (e) {
|
|
1757
|
+
// fall through
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
// durations / dateTimes / floats -> Number (seconds or numeric)
|
|
1761
|
+
const nDur = parseNumOrDuration(t);
|
|
1762
|
+
if (nDur !== null) return { kind: "number", value: nDur };
|
|
1763
|
+
return null;
|
|
1764
|
+
}
|
|
1765
|
+
const n = parseNumOrDuration(t);
|
|
1766
|
+
if (n !== null) return { kind: "number", value: n };
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function cmpNumericInfo(aInfo, bInfo, op) {
|
|
1771
|
+
// op is one of ">", "<", ">=", "<="
|
|
1772
|
+
if (!aInfo || !bInfo) return false;
|
|
1773
|
+
|
|
1774
|
+
if (aInfo.kind === "bigint" && bInfo.kind === "bigint") {
|
|
1775
|
+
if (op === ">") return aInfo.value > bInfo.value;
|
|
1776
|
+
if (op === "<") return aInfo.value < bInfo.value;
|
|
1777
|
+
if (op === ">=") return aInfo.value >= bInfo.value;
|
|
1778
|
+
if (op === "<=") return aInfo.value <= bInfo.value;
|
|
1779
|
+
if (op === "==") return aInfo.value == bInfo.value;
|
|
1780
|
+
if (op === "!=") return aInfo.value != bInfo.value;
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const a = typeof aInfo.value === "bigint" ? Number(aInfo.value) : aInfo.value;
|
|
1785
|
+
const b = typeof bInfo.value === "bigint" ? Number(bInfo.value) : bInfo.value;
|
|
1786
|
+
|
|
1787
|
+
if (op === ">") return a > b;
|
|
1788
|
+
if (op === "<") return a < b;
|
|
1789
|
+
if (op === ">=") return a >= b;
|
|
1790
|
+
if (op === "<=") return a <= b;
|
|
1791
|
+
if (op === "==") return a == b;
|
|
1792
|
+
if (op === "!=") return a != b;
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function parseNumOrDuration(t) {
|
|
1797
|
+
const n = parseNum(t);
|
|
1798
|
+
if (n !== null) return n;
|
|
1799
|
+
if (t instanceof Literal) {
|
|
1800
|
+
let s = t.value;
|
|
1801
|
+
let [lex, dt] = literalParts(s);
|
|
1802
|
+
const val = stripQuotes(lex);
|
|
1803
|
+
if (
|
|
1804
|
+
dt === XSD_NS + "duration" ||
|
|
1805
|
+
val.startsWith("P") ||
|
|
1806
|
+
val.startsWith("-P")
|
|
1807
|
+
) {
|
|
1808
|
+
const negative = val.startsWith("-");
|
|
1809
|
+
const core = negative ? val.slice(1) : val;
|
|
1810
|
+
const secs = parseIso8601DurationToSeconds(core);
|
|
1811
|
+
if (secs === null) return null;
|
|
1812
|
+
return negative ? -secs : secs;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
const dtval = parseDatetimeLike(t);
|
|
1816
|
+
if (dtval !== null) {
|
|
1817
|
+
return dtval.getTime() / 1000.0;
|
|
1818
|
+
}
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function formatDurationLiteralFromSeconds(secs) {
|
|
1823
|
+
const neg = secs < 0;
|
|
1824
|
+
const absSecs = Math.abs(secs);
|
|
1825
|
+
const days = Math.round(absSecs / 86400.0);
|
|
1826
|
+
const lex = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
1827
|
+
const cleanLex = neg ? `" -P${days}D"` : `"P${days}D"`; // minor detail; we just follow shape
|
|
1828
|
+
const lex2 = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
1829
|
+
const actualLex = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
1830
|
+
// keep simpler, no spaces:
|
|
1831
|
+
const finalLex = neg ? `" -P${days}D"` : `"P${days}D"`;
|
|
1832
|
+
const literalLex = neg ? `"-P${days}D"` : `"P${days}D"`;
|
|
1833
|
+
return new Literal(`${literalLex}^^<${XSD_NS}duration>`);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function listAppendSplit(parts, resElems, subst) {
|
|
1837
|
+
if (!parts.length) {
|
|
1838
|
+
if (!resElems.length) return [{ ...subst }];
|
|
1839
|
+
return [];
|
|
1840
|
+
}
|
|
1841
|
+
const out = [];
|
|
1842
|
+
const n = resElems.length;
|
|
1843
|
+
for (let k = 0; k <= n; k++) {
|
|
1844
|
+
const left = new ListTerm(resElems.slice(0, k));
|
|
1845
|
+
let s1 = unifyTerm(parts[0], left, subst);
|
|
1846
|
+
if (s1 === null) continue;
|
|
1847
|
+
const restElems = resElems.slice(k);
|
|
1848
|
+
out.push(...listAppendSplit(parts.slice(1), restElems, s1));
|
|
1849
|
+
}
|
|
1850
|
+
return out;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// ============================================================================
|
|
1854
|
+
// Backward proof & builtins mutual recursion — declarations first
|
|
1855
|
+
// ============================================================================
|
|
1856
|
+
|
|
1857
|
+
function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
|
|
1858
|
+
const g = applySubstTriple(goal, subst);
|
|
1859
|
+
|
|
1860
|
+
function hashLiteral(t, algo) {
|
|
1861
|
+
// Accept only literals, interpret lexical form as UTF-8 string
|
|
1862
|
+
if (!(t instanceof Literal)) return null;
|
|
1863
|
+
const [lex, _dt] = literalParts(t.value);
|
|
1864
|
+
const input = stripQuotes(lex);
|
|
1865
|
+
try {
|
|
1866
|
+
const digest = nodeCrypto
|
|
1867
|
+
.createHash(algo)
|
|
1868
|
+
.update(input, "utf8")
|
|
1869
|
+
.digest("hex");
|
|
1870
|
+
// plain string literal with the hex digest
|
|
1871
|
+
return new Literal(JSON.stringify(digest));
|
|
1872
|
+
} catch (e) {
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// -----------------------------------------------------------------
|
|
1878
|
+
// 4.1 crypto: builtins
|
|
1879
|
+
// -----------------------------------------------------------------
|
|
1880
|
+
|
|
1881
|
+
// crypto:sha
|
|
1882
|
+
// true iff ?o is the SHA-1 hash of the subject string.
|
|
1883
|
+
if (g.p instanceof Iri && g.p.value === CRYPTO_NS + "sha") {
|
|
1884
|
+
const lit = hashLiteral(g.s, "sha1");
|
|
1885
|
+
if (!lit) return [];
|
|
1886
|
+
if (g.o instanceof Var) {
|
|
1887
|
+
const s2 = { ...subst };
|
|
1888
|
+
s2[g.o.name] = lit;
|
|
1889
|
+
return [s2];
|
|
1890
|
+
}
|
|
1891
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1892
|
+
return s2 !== null ? [s2] : [];
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// crypto:md5
|
|
1896
|
+
if (g.p instanceof Iri && g.p.value === CRYPTO_NS + "md5") {
|
|
1897
|
+
const lit = hashLiteral(g.s, "md5");
|
|
1898
|
+
if (!lit) return [];
|
|
1899
|
+
if (g.o instanceof Var) {
|
|
1900
|
+
const s2 = { ...subst };
|
|
1901
|
+
s2[g.o.name] = lit;
|
|
1902
|
+
return [s2];
|
|
1903
|
+
}
|
|
1904
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1905
|
+
return s2 !== null ? [s2] : [];
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// crypto:sha256
|
|
1909
|
+
if (g.p instanceof Iri && g.p.value === CRYPTO_NS + "sha256") {
|
|
1910
|
+
const lit = hashLiteral(g.s, "sha256");
|
|
1911
|
+
if (!lit) return [];
|
|
1912
|
+
if (g.o instanceof Var) {
|
|
1913
|
+
const s2 = { ...subst };
|
|
1914
|
+
s2[g.o.name] = lit;
|
|
1915
|
+
return [s2];
|
|
1916
|
+
}
|
|
1917
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1918
|
+
return s2 !== null ? [s2] : [];
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// crypto:sha512
|
|
1922
|
+
if (g.p instanceof Iri && g.p.value === CRYPTO_NS + "sha512") {
|
|
1923
|
+
const lit = hashLiteral(g.s, "sha512");
|
|
1924
|
+
if (!lit) return [];
|
|
1925
|
+
if (g.o instanceof Var) {
|
|
1926
|
+
const s2 = { ...subst };
|
|
1927
|
+
s2[g.o.name] = lit;
|
|
1928
|
+
return [s2];
|
|
1929
|
+
}
|
|
1930
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
1931
|
+
return s2 !== null ? [s2] : [];
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// -----------------------------------------------------------------
|
|
1935
|
+
// 4.2 math: builtins
|
|
1936
|
+
// -----------------------------------------------------------------
|
|
1937
|
+
|
|
1938
|
+
// math:greaterThan
|
|
1939
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "greaterThan") {
|
|
1940
|
+
const aInfo = parseNumericForCompareTerm(g.s);
|
|
1941
|
+
const bInfo = parseNumericForCompareTerm(g.o);
|
|
1942
|
+
if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, ">")) return [{ ...subst }];
|
|
1943
|
+
|
|
1944
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
1945
|
+
const a2 = parseNumericForCompareTerm(g.s.elems[0]);
|
|
1946
|
+
const b2 = parseNumericForCompareTerm(g.s.elems[1]);
|
|
1947
|
+
if (a2 && b2 && cmpNumericInfo(a2, b2, ">")) return [{ ...subst }];
|
|
1948
|
+
}
|
|
1949
|
+
return [];
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// math:lessThan
|
|
1953
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "lessThan") {
|
|
1954
|
+
const aInfo = parseNumericForCompareTerm(g.s);
|
|
1955
|
+
const bInfo = parseNumericForCompareTerm(g.o);
|
|
1956
|
+
if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, "<")) return [{ ...subst }];
|
|
1957
|
+
|
|
1958
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
1959
|
+
const a2 = parseNumericForCompareTerm(g.s.elems[0]);
|
|
1960
|
+
const b2 = parseNumericForCompareTerm(g.s.elems[1]);
|
|
1961
|
+
if (a2 && b2 && cmpNumericInfo(a2, b2, "<")) return [{ ...subst }];
|
|
1962
|
+
}
|
|
1963
|
+
return [];
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// math:notLessThan
|
|
1967
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "notLessThan") {
|
|
1968
|
+
const aInfo = parseNumericForCompareTerm(g.s);
|
|
1969
|
+
const bInfo = parseNumericForCompareTerm(g.o);
|
|
1970
|
+
if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, ">=")) return [{ ...subst }];
|
|
1971
|
+
|
|
1972
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
1973
|
+
const a2 = parseNumericForCompareTerm(g.s.elems[0]);
|
|
1974
|
+
const b2 = parseNumericForCompareTerm(g.s.elems[1]);
|
|
1975
|
+
if (a2 && b2 && cmpNumericInfo(a2, b2, ">=")) return [{ ...subst }];
|
|
1976
|
+
}
|
|
1977
|
+
return [];
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// math:notGreaterThan
|
|
1981
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "notGreaterThan") {
|
|
1982
|
+
const aInfo = parseNumericForCompareTerm(g.s);
|
|
1983
|
+
const bInfo = parseNumericForCompareTerm(g.o);
|
|
1984
|
+
if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, "<=")) return [{ ...subst }];
|
|
1985
|
+
|
|
1986
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
1987
|
+
const a2 = parseNumericForCompareTerm(g.s.elems[0]);
|
|
1988
|
+
const b2 = parseNumericForCompareTerm(g.s.elems[1]);
|
|
1989
|
+
if (a2 && b2 && cmpNumericInfo(a2, b2, "<=")) return [{ ...subst }];
|
|
1990
|
+
}
|
|
1991
|
+
return [];
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// math:equalTo
|
|
1995
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "equalTo") {
|
|
1996
|
+
const aInfo = parseNumericForCompareTerm(g.s);
|
|
1997
|
+
const bInfo = parseNumericForCompareTerm(g.o);
|
|
1998
|
+
if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, "==")) return [{ ...subst }];
|
|
1999
|
+
|
|
2000
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
2001
|
+
const a2 = parseNumericForCompareTerm(g.s.elems[0]);
|
|
2002
|
+
const b2 = parseNumericForCompareTerm(g.s.elems[1]);
|
|
2003
|
+
if (a2 && b2 && cmpNumericInfo(a2, b2, "==")) return [{ ...subst }];
|
|
2004
|
+
}
|
|
2005
|
+
return [];
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// math:notEqualTo
|
|
2009
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "notEqualTo") {
|
|
2010
|
+
const aInfo = parseNumericForCompareTerm(g.s);
|
|
2011
|
+
const bInfo = parseNumericForCompareTerm(g.o);
|
|
2012
|
+
if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, "!=")) return [{ ...subst }];
|
|
2013
|
+
|
|
2014
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
2015
|
+
const a2 = parseNumericForCompareTerm(g.s.elems[0]);
|
|
2016
|
+
const b2 = parseNumericForCompareTerm(g.s.elems[1]);
|
|
2017
|
+
if (a2 && b2 && cmpNumericInfo(a2, b2, "!=")) return [{ ...subst }];
|
|
2018
|
+
}
|
|
2019
|
+
return [];
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// math:sum
|
|
2023
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "sum") {
|
|
2024
|
+
if (g.s instanceof ListTerm && g.s.elems.length >= 2) {
|
|
2025
|
+
const xs = g.s.elems;
|
|
2026
|
+
const values = [];
|
|
2027
|
+
for (const t of xs) {
|
|
2028
|
+
const v = parseNumberLiteral(t);
|
|
2029
|
+
if (v === null) return [];
|
|
2030
|
+
values.push(v);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
let lit;
|
|
2034
|
+
const allBig = values.every(v => typeof v === "bigint");
|
|
2035
|
+
if (allBig) {
|
|
2036
|
+
let total = 0n;
|
|
2037
|
+
for (const v of values) total += v;
|
|
2038
|
+
lit = new Literal(total.toString());
|
|
2039
|
+
} else {
|
|
2040
|
+
let total = 0.0;
|
|
2041
|
+
for (const v of values) {
|
|
2042
|
+
total += typeof v === "bigint" ? Number(v) : v;
|
|
2043
|
+
}
|
|
2044
|
+
lit = new Literal(formatNum(total));
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
if (g.o instanceof Var) {
|
|
2048
|
+
const s2 = { ...subst };
|
|
2049
|
+
s2[g.o.name] = lit;
|
|
2050
|
+
return [s2];
|
|
2051
|
+
}
|
|
2052
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
2053
|
+
return s2 !== null ? [s2] : [];
|
|
2054
|
+
}
|
|
2055
|
+
return [];
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// math:product
|
|
2059
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "product") {
|
|
2060
|
+
if (g.s instanceof ListTerm && g.s.elems.length >= 2) {
|
|
2061
|
+
const xs = g.s.elems;
|
|
2062
|
+
const values = [];
|
|
2063
|
+
for (const t of xs) {
|
|
2064
|
+
const v = parseNumberLiteral(t);
|
|
2065
|
+
if (v === null) return [];
|
|
2066
|
+
values.push(v);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
let lit;
|
|
2070
|
+
const allBig = values.every(v => typeof v === "bigint");
|
|
2071
|
+
if (allBig) {
|
|
2072
|
+
let prod = 1n;
|
|
2073
|
+
for (const v of values) prod *= v;
|
|
2074
|
+
lit = new Literal(prod.toString());
|
|
2075
|
+
} else {
|
|
2076
|
+
let prod = 1.0;
|
|
2077
|
+
for (const v of values) {
|
|
2078
|
+
prod *= typeof v === "bigint" ? Number(v) : v;
|
|
2079
|
+
}
|
|
2080
|
+
lit = new Literal(formatNum(prod));
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
if (g.o instanceof Var) {
|
|
2084
|
+
const s2 = { ...subst };
|
|
2085
|
+
s2[g.o.name] = lit;
|
|
2086
|
+
return [s2];
|
|
2087
|
+
}
|
|
2088
|
+
if (g.o instanceof Literal && g.o.value === lit.value) {
|
|
2089
|
+
return [{ ...subst }];
|
|
2090
|
+
}
|
|
2091
|
+
return [];
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// math:difference
|
|
2096
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "difference") {
|
|
2097
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
2098
|
+
const [a0, b0] = g.s.elems;
|
|
2099
|
+
|
|
2100
|
+
// BigInt integer difference
|
|
2101
|
+
const ai = parseIntLiteral(a0);
|
|
2102
|
+
const bi = parseIntLiteral(b0);
|
|
2103
|
+
if (ai !== null && bi !== null) {
|
|
2104
|
+
const ci = ai - bi;
|
|
2105
|
+
const lit = new Literal(ci.toString());
|
|
2106
|
+
if (g.o instanceof Var) {
|
|
2107
|
+
const s2 = { ...subst };
|
|
2108
|
+
s2[g.o.name] = lit;
|
|
2109
|
+
return [s2];
|
|
2110
|
+
} else {
|
|
2111
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
2112
|
+
return s2 !== null ? [s2] : [];
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Numeric difference via floats
|
|
2117
|
+
const a = parseNum(a0);
|
|
2118
|
+
const b = parseNum(b0);
|
|
2119
|
+
if (a !== null && b !== null) {
|
|
2120
|
+
const c = a - b;
|
|
2121
|
+
if (g.o instanceof Var) {
|
|
2122
|
+
const s2 = { ...subst };
|
|
2123
|
+
s2[g.o.name] = new Literal(formatNum(c));
|
|
2124
|
+
return [s2];
|
|
2125
|
+
}
|
|
2126
|
+
if (g.o instanceof Literal && g.o.value === formatNum(c)) {
|
|
2127
|
+
return [{ ...subst }];
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// Date/datetime difference -> duration
|
|
2132
|
+
const aDt = parseDatetimeLike(a0);
|
|
2133
|
+
const bDt = parseDatetimeLike(b0);
|
|
2134
|
+
if (aDt !== null && bDt !== null) {
|
|
2135
|
+
const diffSecs = (aDt.getTime() - bDt.getTime()) / 1000.0;
|
|
2136
|
+
const durTerm = formatDurationLiteralFromSeconds(diffSecs);
|
|
2137
|
+
if (g.o instanceof Var) {
|
|
2138
|
+
const s2 = { ...subst };
|
|
2139
|
+
s2[g.o.name] = durTerm;
|
|
2140
|
+
return [s2];
|
|
2141
|
+
}
|
|
2142
|
+
if (g.o instanceof Literal && g.o.value === durTerm.value) {
|
|
2143
|
+
return [{ ...subst }];
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
return [];
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
// math:quotient
|
|
2151
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "quotient") {
|
|
2152
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
2153
|
+
const a = parseNum(g.s.elems[0]);
|
|
2154
|
+
const b = parseNum(g.s.elems[1]);
|
|
2155
|
+
if (a !== null && b !== null && b !== 0.0) {
|
|
2156
|
+
const c = a / b;
|
|
2157
|
+
if (g.o instanceof Var) {
|
|
2158
|
+
const s2 = { ...subst };
|
|
2159
|
+
s2[g.o.name] = new Literal(formatNum(c));
|
|
2160
|
+
return [s2];
|
|
2161
|
+
}
|
|
2162
|
+
if (g.o instanceof Literal && g.o.value === formatNum(c)) {
|
|
2163
|
+
return [{ ...subst }];
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
return [];
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// math:exponentiation
|
|
2171
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "exponentiation") {
|
|
2172
|
+
if (g.s instanceof ListTerm && g.s.elems.length === 2) {
|
|
2173
|
+
const a = parseNum(g.s.elems[0]);
|
|
2174
|
+
const b0 = g.s.elems[1];
|
|
2175
|
+
const c = parseNum(g.o);
|
|
2176
|
+
let b = null;
|
|
2177
|
+
if (a !== null && b0 instanceof Literal) b = parseNum(b0);
|
|
2178
|
+
|
|
2179
|
+
if (a !== null && b !== null) {
|
|
2180
|
+
const cVal = a ** b;
|
|
2181
|
+
if (g.o instanceof Var) {
|
|
2182
|
+
const s2 = { ...subst };
|
|
2183
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2184
|
+
return [s2];
|
|
2185
|
+
}
|
|
2186
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2187
|
+
return [{ ...subst }];
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// inverse mode
|
|
2192
|
+
if (a !== null && b0 instanceof Var && c !== null) {
|
|
2193
|
+
if (a > 0.0 && a !== 1.0 && c > 0.0) {
|
|
2194
|
+
const bVal = Math.log(c) / Math.log(a);
|
|
2195
|
+
const s2 = { ...subst };
|
|
2196
|
+
s2[b0.name] = new Literal(formatNum(bVal));
|
|
2197
|
+
return [s2];
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return [];
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// math:negation
|
|
2205
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "negation") {
|
|
2206
|
+
const a = parseNum(g.s);
|
|
2207
|
+
if (a !== null && g.o instanceof Var) {
|
|
2208
|
+
const s2 = { ...subst };
|
|
2209
|
+
s2[g.o.name] = new Literal(formatNum(-a));
|
|
2210
|
+
return [s2];
|
|
2211
|
+
}
|
|
2212
|
+
const b = parseNum(g.o);
|
|
2213
|
+
if (g.s instanceof Var && b !== null) {
|
|
2214
|
+
const s2 = { ...subst };
|
|
2215
|
+
s2[g.s.name] = new Literal(formatNum(-b));
|
|
2216
|
+
return [s2];
|
|
2217
|
+
}
|
|
2218
|
+
const a2 = parseNum(g.s);
|
|
2219
|
+
const b2 = parseNum(g.o);
|
|
2220
|
+
if (a2 !== null && b2 !== null) {
|
|
2221
|
+
if (Math.abs(-a2 - b2) < 1e-9) return [{ ...subst }];
|
|
2222
|
+
}
|
|
2223
|
+
return [];
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
// math:absoluteValue
|
|
2227
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "absoluteValue") {
|
|
2228
|
+
const a = parseNum(g.s);
|
|
2229
|
+
if (a !== null && g.o instanceof Var) {
|
|
2230
|
+
const s2 = { ...subst };
|
|
2231
|
+
s2[g.o.name] = new Literal(formatNum(Math.abs(a)));
|
|
2232
|
+
return [s2];
|
|
2233
|
+
}
|
|
2234
|
+
const b = parseNum(g.o);
|
|
2235
|
+
if (a !== null && b !== null) {
|
|
2236
|
+
if (Math.abs(Math.abs(a) - b) < 1e-9) return [{ ...subst }];
|
|
2237
|
+
}
|
|
2238
|
+
return [];
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// math:cos
|
|
2242
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "cos") {
|
|
2243
|
+
const a = parseNum(g.s);
|
|
2244
|
+
if (a !== null) {
|
|
2245
|
+
const cVal = Math.cos(a);
|
|
2246
|
+
if (g.o instanceof Var) {
|
|
2247
|
+
const s2 = { ...subst };
|
|
2248
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2249
|
+
return [s2];
|
|
2250
|
+
}
|
|
2251
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2252
|
+
return [{ ...subst }];
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return [];
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// math:sin
|
|
2259
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "sin") {
|
|
2260
|
+
const a = parseNum(g.s);
|
|
2261
|
+
if (a !== null) {
|
|
2262
|
+
const cVal = Math.sin(a);
|
|
2263
|
+
if (g.o instanceof Var) {
|
|
2264
|
+
const s2 = { ...subst };
|
|
2265
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2266
|
+
return [s2];
|
|
2267
|
+
}
|
|
2268
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2269
|
+
return [{ ...subst }];
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return [];
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// math:acos
|
|
2276
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "acos") {
|
|
2277
|
+
const a = parseNum(g.s);
|
|
2278
|
+
if (a !== null) {
|
|
2279
|
+
const cVal = Math.acos(a);
|
|
2280
|
+
if (Number.isFinite(cVal)) {
|
|
2281
|
+
if (g.o instanceof Var) {
|
|
2282
|
+
const s2 = { ...subst };
|
|
2283
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2284
|
+
return [s2];
|
|
2285
|
+
}
|
|
2286
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2287
|
+
return [{ ...subst }];
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return [];
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// math:asin
|
|
2295
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "asin") {
|
|
2296
|
+
const a = parseNum(g.s);
|
|
2297
|
+
if (a !== null) {
|
|
2298
|
+
const cVal = Math.asin(a);
|
|
2299
|
+
if (Number.isFinite(cVal)) {
|
|
2300
|
+
if (g.o instanceof Var) {
|
|
2301
|
+
const s2 = { ...subst };
|
|
2302
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2303
|
+
return [s2];
|
|
2304
|
+
}
|
|
2305
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2306
|
+
return [{ ...subst }];
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
return [];
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// math:atan
|
|
2314
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "atan") {
|
|
2315
|
+
const a = parseNum(g.s);
|
|
2316
|
+
if (a !== null) {
|
|
2317
|
+
const cVal = Math.atan(a);
|
|
2318
|
+
if (Number.isFinite(cVal)) {
|
|
2319
|
+
if (g.o instanceof Var) {
|
|
2320
|
+
const s2 = { ...subst };
|
|
2321
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2322
|
+
return [s2];
|
|
2323
|
+
}
|
|
2324
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2325
|
+
return [{ ...subst }];
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return [];
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// math:cosh
|
|
2333
|
+
// Hyperbolic cosine
|
|
2334
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "cosh") {
|
|
2335
|
+
const a = parseNum(g.s);
|
|
2336
|
+
if (a !== null && typeof Math.cosh === "function") {
|
|
2337
|
+
const cVal = Math.cosh(a);
|
|
2338
|
+
if (Number.isFinite(cVal)) {
|
|
2339
|
+
if (g.o instanceof Var) {
|
|
2340
|
+
const s2 = { ...subst };
|
|
2341
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2342
|
+
return [s2];
|
|
2343
|
+
}
|
|
2344
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2345
|
+
return [{ ...subst }];
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return [];
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// math:degrees
|
|
2353
|
+
// Convert radians -> degrees
|
|
2354
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "degrees") {
|
|
2355
|
+
const a = parseNum(g.s);
|
|
2356
|
+
if (a !== null) {
|
|
2357
|
+
const cVal = (a * 180.0) / Math.PI;
|
|
2358
|
+
if (Number.isFinite(cVal)) {
|
|
2359
|
+
if (g.o instanceof Var) {
|
|
2360
|
+
const s2 = { ...subst };
|
|
2361
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2362
|
+
return [s2];
|
|
2363
|
+
}
|
|
2364
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2365
|
+
return [{ ...subst }];
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return [];
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// math:remainder
|
|
2373
|
+
// Subject is a list (dividend divisor); object is the remainder.
|
|
2374
|
+
// Schema: ( $a $b ) math:remainder $r
|
|
2375
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "remainder") {
|
|
2376
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
2377
|
+
const a = parseNum(g.s.elems[0]);
|
|
2378
|
+
const b = parseNum(g.s.elems[1]);
|
|
2379
|
+
if (a === null || b === null || b === 0) return [];
|
|
2380
|
+
const rVal = a % b;
|
|
2381
|
+
if (!Number.isFinite(rVal)) return [];
|
|
2382
|
+
const lit = new Literal(formatNum(rVal));
|
|
2383
|
+
|
|
2384
|
+
if (g.o instanceof Var) {
|
|
2385
|
+
const s2 = { ...subst };
|
|
2386
|
+
s2[g.o.name] = lit;
|
|
2387
|
+
return [s2];
|
|
2388
|
+
}
|
|
2389
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
2390
|
+
return s2 !== null ? [s2] : [];
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// math:rounded
|
|
2394
|
+
// Round to nearest integer.
|
|
2395
|
+
// If there are two such numbers, then the one closest to positive infinity is returned.
|
|
2396
|
+
// Schema: $s math:rounded $o
|
|
2397
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "rounded") {
|
|
2398
|
+
const a = parseNum(g.s);
|
|
2399
|
+
if (a === null) return [];
|
|
2400
|
+
const rVal = Math.round(a);
|
|
2401
|
+
const lit = new Literal(formatNum(rVal));
|
|
2402
|
+
|
|
2403
|
+
if (g.o instanceof Var) {
|
|
2404
|
+
const s2 = { ...subst };
|
|
2405
|
+
s2[g.o.name] = lit;
|
|
2406
|
+
return [s2];
|
|
2407
|
+
}
|
|
2408
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
2409
|
+
return s2 !== null ? [s2] : [];
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// math:sinh
|
|
2413
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "sinh") {
|
|
2414
|
+
const a = parseNum(g.s);
|
|
2415
|
+
if (a !== null && typeof Math.sinh === "function") {
|
|
2416
|
+
const cVal = Math.sinh(a);
|
|
2417
|
+
if (Number.isFinite(cVal)) {
|
|
2418
|
+
if (g.o instanceof Var) {
|
|
2419
|
+
const s2 = { ...subst };
|
|
2420
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2421
|
+
return [s2];
|
|
2422
|
+
}
|
|
2423
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2424
|
+
return [{ ...subst }];
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return [];
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// math:tan
|
|
2432
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "tan") {
|
|
2433
|
+
const a = parseNum(g.s);
|
|
2434
|
+
if (a !== null) {
|
|
2435
|
+
const cVal = Math.tan(a);
|
|
2436
|
+
if (Number.isFinite(cVal)) {
|
|
2437
|
+
if (g.o instanceof Var) {
|
|
2438
|
+
const s2 = { ...subst };
|
|
2439
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2440
|
+
return [s2];
|
|
2441
|
+
}
|
|
2442
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2443
|
+
return [{ ...subst }];
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return [];
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// math:tanh
|
|
2451
|
+
if (g.p instanceof Iri && g.p.value === MATH_NS + "tanh") {
|
|
2452
|
+
const a = parseNum(g.s);
|
|
2453
|
+
if (a !== null && typeof Math.tanh === "function") {
|
|
2454
|
+
const cVal = Math.tanh(a);
|
|
2455
|
+
if (Number.isFinite(cVal)) {
|
|
2456
|
+
if (g.o instanceof Var) {
|
|
2457
|
+
const s2 = { ...subst };
|
|
2458
|
+
s2[g.o.name] = new Literal(formatNum(cVal));
|
|
2459
|
+
return [s2];
|
|
2460
|
+
}
|
|
2461
|
+
if (g.o instanceof Literal && g.o.value === formatNum(cVal)) {
|
|
2462
|
+
return [{ ...subst }];
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
return [];
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// -----------------------------------------------------------------
|
|
2470
|
+
// 4.3 time: builtins
|
|
2471
|
+
// -----------------------------------------------------------------
|
|
2472
|
+
|
|
2473
|
+
// time:localTime
|
|
2474
|
+
// "" time:localTime ?D. binds ?D to “now” as xsd:dateTime.
|
|
2475
|
+
if (g.p instanceof Iri && g.p.value === TIME_NS + "localTime") {
|
|
2476
|
+
const now = localIsoDateTimeString(new Date());
|
|
2477
|
+
if (g.o instanceof Var) {
|
|
2478
|
+
const s2 = { ...subst };
|
|
2479
|
+
s2[g.o.name] = new Literal(`"${now}"^^<${XSD_NS}dateTime>`);
|
|
2480
|
+
return [s2];
|
|
2481
|
+
}
|
|
2482
|
+
if (g.o instanceof Literal) {
|
|
2483
|
+
const [lexO] = literalParts(g.o.value);
|
|
2484
|
+
if (stripQuotes(lexO) === now) return [{ ...subst }];
|
|
2485
|
+
}
|
|
2486
|
+
return [];
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// -----------------------------------------------------------------
|
|
2490
|
+
// 4.4 list: builtins
|
|
2491
|
+
// -----------------------------------------------------------------
|
|
2492
|
+
|
|
2493
|
+
// list:append
|
|
2494
|
+
// true if and only if $o is the concatenation of all lists $s.i.
|
|
2495
|
+
// Schema: ( $s.i?[*] )+ list:append $o?
|
|
2496
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "append") {
|
|
2497
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2498
|
+
const parts = g.s.elems;
|
|
2499
|
+
if (g.o instanceof ListTerm) {
|
|
2500
|
+
return listAppendSplit(parts, g.o.elems, subst);
|
|
2501
|
+
}
|
|
2502
|
+
const outElems = [];
|
|
2503
|
+
for (const part of parts) {
|
|
2504
|
+
if (!(part instanceof ListTerm)) return [];
|
|
2505
|
+
outElems.push(...part.elems);
|
|
2506
|
+
}
|
|
2507
|
+
const result = new ListTerm(outElems);
|
|
2508
|
+
if (g.o instanceof Var) {
|
|
2509
|
+
const s2 = { ...subst };
|
|
2510
|
+
s2[g.o.name] = result;
|
|
2511
|
+
return [s2];
|
|
2512
|
+
}
|
|
2513
|
+
if (termsEqual(g.o, result)) return [{ ...subst }];
|
|
2514
|
+
return [];
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// list:first
|
|
2518
|
+
// true iff $s is a list and $o is the first member of that list.
|
|
2519
|
+
// Schema: $s+ list:first $o-
|
|
2520
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "first") {
|
|
2521
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2522
|
+
if (!g.s.elems.length) return [];
|
|
2523
|
+
const first = g.s.elems[0];
|
|
2524
|
+
const s2 = unifyTerm(g.o, first, subst);
|
|
2525
|
+
return s2 !== null ? [s2] : [];
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// list:iterate
|
|
2529
|
+
// true iff $s is a list and $o is a list (index value),
|
|
2530
|
+
// where index is a valid 0-based index into $s and value is the element at that index.
|
|
2531
|
+
// Schema: $s+ list:iterate ( $o.1?[*] $o.2?[*] )?[*]
|
|
2532
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "iterate") {
|
|
2533
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2534
|
+
if (!(g.o instanceof ListTerm) || g.o.elems.length !== 2) return [];
|
|
2535
|
+
const [idxTerm, valTerm] = g.o.elems;
|
|
2536
|
+
const xs = g.s.elems;
|
|
2537
|
+
const outs = [];
|
|
2538
|
+
|
|
2539
|
+
for (let i = 0; i < xs.length; i++) {
|
|
2540
|
+
const idxLit = new Literal(String(i)); // index starts at 0
|
|
2541
|
+
let s1 = unifyTerm(idxTerm, idxLit, subst);
|
|
2542
|
+
if (s1 === null) continue;
|
|
2543
|
+
let s2 = unifyTerm(valTerm, xs[i], s1);
|
|
2544
|
+
if (s2 === null) continue;
|
|
2545
|
+
outs.push(s2);
|
|
2546
|
+
}
|
|
2547
|
+
return outs;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// list:last
|
|
2551
|
+
// true iff $s is a list and $o is the last member of that list.
|
|
2552
|
+
// Schema: $s+ list:last $o-
|
|
2553
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "last") {
|
|
2554
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2555
|
+
const xs = g.s.elems;
|
|
2556
|
+
if (!xs.length) return [];
|
|
2557
|
+
const last = xs[xs.length - 1];
|
|
2558
|
+
const s2 = unifyTerm(g.o, last, subst);
|
|
2559
|
+
return s2 !== null ? [s2] : [];
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
// list:memberAt
|
|
2563
|
+
// true iff $s.1 is a list, $s.2 is a valid index, and $o is the member at that index.
|
|
2564
|
+
// Schema: ( $s.1+ $s.2?[*] )+ list:memberAt $o?[*]
|
|
2565
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "memberAt") {
|
|
2566
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
2567
|
+
const [listTerm, indexTerm] = g.s.elems;
|
|
2568
|
+
if (!(listTerm instanceof ListTerm)) return [];
|
|
2569
|
+
const xs = listTerm.elems;
|
|
2570
|
+
const outs = [];
|
|
2571
|
+
|
|
2572
|
+
for (let i = 0; i < xs.length; i++) {
|
|
2573
|
+
const idxLit = new Literal(String(i)); // index starts at 0
|
|
2574
|
+
let s1 = unifyTerm(indexTerm, idxLit, subst);
|
|
2575
|
+
if (s1 === null) continue;
|
|
2576
|
+
let s2 = unifyTerm(g.o, xs[i], s1);
|
|
2577
|
+
if (s2 === null) continue;
|
|
2578
|
+
outs.push(s2);
|
|
2579
|
+
}
|
|
2580
|
+
return outs;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// list:remove
|
|
2584
|
+
// true iff $s.1 is a list and $o is that list with all occurrences of $s.2 removed.
|
|
2585
|
+
// Schema: ( $s.1+ $s.2+ )+ list:remove $o-
|
|
2586
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "remove") {
|
|
2587
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
2588
|
+
const [listTerm, itemTerm] = g.s.elems;
|
|
2589
|
+
if (!(listTerm instanceof ListTerm)) return [];
|
|
2590
|
+
const xs = listTerm.elems;
|
|
2591
|
+
const filtered = [];
|
|
2592
|
+
for (const e of xs) {
|
|
2593
|
+
if (!termsEqual(e, itemTerm)) filtered.push(e);
|
|
2594
|
+
}
|
|
2595
|
+
const resList = new ListTerm(filtered);
|
|
2596
|
+
const s2 = unifyTerm(g.o, resList, subst);
|
|
2597
|
+
return s2 !== null ? [s2] : [];
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// list:member
|
|
2601
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "member") {
|
|
2602
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2603
|
+
const outs = [];
|
|
2604
|
+
for (const x of g.s.elems) {
|
|
2605
|
+
const s2 = unifyTerm(g.o, x, subst);
|
|
2606
|
+
if (s2 !== null) outs.push(s2);
|
|
2607
|
+
}
|
|
2608
|
+
return outs;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// list:in
|
|
2612
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "in") {
|
|
2613
|
+
if (!(g.o instanceof ListTerm)) return [];
|
|
2614
|
+
const outs = [];
|
|
2615
|
+
for (const x of g.o.elems) {
|
|
2616
|
+
const s2 = unifyTerm(g.s, x, subst);
|
|
2617
|
+
if (s2 !== null) outs.push(s2);
|
|
2618
|
+
}
|
|
2619
|
+
return outs;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// list:length
|
|
2623
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "length") {
|
|
2624
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2625
|
+
const nTerm = new Literal(String(g.s.elems.length));
|
|
2626
|
+
const s2 = unifyTerm(g.o, nTerm, subst);
|
|
2627
|
+
return s2 !== null ? [s2] : [];
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// list:notMember
|
|
2631
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "notMember") {
|
|
2632
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2633
|
+
for (const el of g.s.elems) {
|
|
2634
|
+
if (unifyTerm(g.o, el, subst) !== null) return [];
|
|
2635
|
+
}
|
|
2636
|
+
return [{ ...subst }];
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// list:reverse
|
|
2640
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "reverse") {
|
|
2641
|
+
if (g.s instanceof ListTerm) {
|
|
2642
|
+
const rev = [...g.s.elems].reverse();
|
|
2643
|
+
const rterm = new ListTerm(rev);
|
|
2644
|
+
const s2 = unifyTerm(g.o, rterm, subst);
|
|
2645
|
+
return s2 !== null ? [s2] : [];
|
|
2646
|
+
}
|
|
2647
|
+
if (g.o instanceof ListTerm) {
|
|
2648
|
+
const rev = [...g.o.elems].reverse();
|
|
2649
|
+
const rterm = new ListTerm(rev);
|
|
2650
|
+
const s2 = unifyTerm(g.s, rterm, subst);
|
|
2651
|
+
return s2 !== null ? [s2] : [];
|
|
2652
|
+
}
|
|
2653
|
+
return [];
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
// list:sort
|
|
2657
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "sort") {
|
|
2658
|
+
function cmpTermForSort(a, b) {
|
|
2659
|
+
if (a instanceof Literal && b instanceof Literal) {
|
|
2660
|
+
const [lexA] = literalParts(a.value);
|
|
2661
|
+
const [lexB] = literalParts(b.value);
|
|
2662
|
+
const sa = stripQuotes(lexA);
|
|
2663
|
+
const sb = stripQuotes(lexB);
|
|
2664
|
+
const na = Number(sa);
|
|
2665
|
+
const nb = Number(sb);
|
|
2666
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
|
2667
|
+
if (na < nb) return -1;
|
|
2668
|
+
if (na > nb) return 1;
|
|
2669
|
+
return 0;
|
|
2670
|
+
}
|
|
2671
|
+
if (sa < sb) return -1;
|
|
2672
|
+
if (sa > sb) return 1;
|
|
2673
|
+
return 0;
|
|
2674
|
+
}
|
|
2675
|
+
if (a instanceof ListTerm && b instanceof ListTerm) {
|
|
2676
|
+
const xs = a.elems;
|
|
2677
|
+
const ys = b.elems;
|
|
2678
|
+
let i = 0;
|
|
2679
|
+
// lexicographic
|
|
2680
|
+
while (true) {
|
|
2681
|
+
if (i >= xs.length && i >= ys.length) return 0;
|
|
2682
|
+
if (i >= xs.length) return -1;
|
|
2683
|
+
if (i >= ys.length) return 1;
|
|
2684
|
+
const c = cmpTermForSort(xs[i], ys[i]);
|
|
2685
|
+
if (c !== 0) return c;
|
|
2686
|
+
i++;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
if (a instanceof Iri && b instanceof Iri) {
|
|
2690
|
+
if (a.value < b.value) return -1;
|
|
2691
|
+
if (a.value > b.value) return 1;
|
|
2692
|
+
return 0;
|
|
2693
|
+
}
|
|
2694
|
+
// lists before non-lists
|
|
2695
|
+
if (a instanceof ListTerm && !(b instanceof ListTerm)) return -1;
|
|
2696
|
+
if (!(a instanceof ListTerm) && b instanceof ListTerm) return 1;
|
|
2697
|
+
const sa = JSON.stringify(a);
|
|
2698
|
+
const sb = JSON.stringify(b);
|
|
2699
|
+
if (sa < sb) return -1;
|
|
2700
|
+
if (sa > sb) return 1;
|
|
2701
|
+
return 0;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
let inputList;
|
|
2705
|
+
if (g.s instanceof ListTerm) inputList = g.s.elems;
|
|
2706
|
+
else if (g.o instanceof ListTerm) inputList = g.o.elems;
|
|
2707
|
+
else return [];
|
|
2708
|
+
|
|
2709
|
+
if (!inputList.every(e => isGroundTerm(e))) return [];
|
|
2710
|
+
|
|
2711
|
+
const sortedList = [...inputList].sort(cmpTermForSort);
|
|
2712
|
+
const sortedTerm = new ListTerm(sortedList);
|
|
2713
|
+
if (g.s instanceof ListTerm) {
|
|
2714
|
+
const s2 = unifyTerm(g.o, sortedTerm, subst);
|
|
2715
|
+
return s2 !== null ? [s2] : [];
|
|
2716
|
+
}
|
|
2717
|
+
if (g.o instanceof ListTerm) {
|
|
2718
|
+
const s2 = unifyTerm(g.s, sortedTerm, subst);
|
|
2719
|
+
return s2 !== null ? [s2] : [];
|
|
2720
|
+
}
|
|
2721
|
+
return [];
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// list:map
|
|
2725
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "map") {
|
|
2726
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
2727
|
+
const [inputTerm, predTerm] = g.s.elems;
|
|
2728
|
+
if (!(inputTerm instanceof ListTerm)) return [];
|
|
2729
|
+
const inputList = inputTerm.elems;
|
|
2730
|
+
if (!(predTerm instanceof Iri)) return [];
|
|
2731
|
+
const pred = new Iri(predTerm.value);
|
|
2732
|
+
if (!isBuiltinPred(pred)) return [];
|
|
2733
|
+
if (!inputList.every(e => isGroundTerm(e))) return [];
|
|
2734
|
+
|
|
2735
|
+
const results = [];
|
|
2736
|
+
for (const el of inputList) {
|
|
2737
|
+
const yvar = new Var("_mapY");
|
|
2738
|
+
const goal2 = new Triple(el, pred, yvar);
|
|
2739
|
+
const sols = evalBuiltin(goal2, subst, facts, backRules, depth + 1, varGen);
|
|
2740
|
+
if (!sols.length) return [];
|
|
2741
|
+
const yval = applySubstTerm(yvar, sols[0]);
|
|
2742
|
+
if (yval instanceof Var) return [];
|
|
2743
|
+
results.push(yval);
|
|
2744
|
+
}
|
|
2745
|
+
const outList = new ListTerm(results);
|
|
2746
|
+
const s2 = unifyTerm(g.o, outList, subst);
|
|
2747
|
+
return s2 !== null ? [s2] : [];
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
// list:firstRest
|
|
2751
|
+
if (g.p instanceof Iri && g.p.value === LIST_NS + "firstRest") {
|
|
2752
|
+
if (g.s instanceof ListTerm) {
|
|
2753
|
+
if (!g.s.elems.length) return [];
|
|
2754
|
+
const first = g.s.elems[0];
|
|
2755
|
+
const rest = new ListTerm(g.s.elems.slice(1));
|
|
2756
|
+
const pair = new ListTerm([first, rest]);
|
|
2757
|
+
const s2 = unifyTerm(g.o, pair, subst);
|
|
2758
|
+
return s2 !== null ? [s2] : [];
|
|
2759
|
+
}
|
|
2760
|
+
if (g.o instanceof ListTerm && g.o.elems.length === 2) {
|
|
2761
|
+
const first = g.o.elems[0];
|
|
2762
|
+
const rest = g.o.elems[1];
|
|
2763
|
+
if (rest instanceof ListTerm) {
|
|
2764
|
+
const xs = [first, ...rest.elems];
|
|
2765
|
+
const constructed = new ListTerm(xs);
|
|
2766
|
+
const s2 = unifyTerm(g.s, constructed, subst);
|
|
2767
|
+
return s2 !== null ? [s2] : [];
|
|
2768
|
+
}
|
|
2769
|
+
if (rest instanceof Var) {
|
|
2770
|
+
const constructed = new OpenListTerm([first], rest.name);
|
|
2771
|
+
const s2 = unifyTerm(g.s, constructed, subst);
|
|
2772
|
+
return s2 !== null ? [s2] : [];
|
|
2773
|
+
}
|
|
2774
|
+
if (rest instanceof OpenListTerm) {
|
|
2775
|
+
const newPrefix = [first, ...rest.prefix];
|
|
2776
|
+
const constructed = new OpenListTerm(newPrefix, rest.tailVar);
|
|
2777
|
+
const s2 = unifyTerm(g.s, constructed, subst);
|
|
2778
|
+
return s2 !== null ? [s2] : [];
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return [];
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// -----------------------------------------------------------------
|
|
2785
|
+
// 4.5 log: builtins
|
|
2786
|
+
// -----------------------------------------------------------------
|
|
2787
|
+
|
|
2788
|
+
// log:equalTo
|
|
2789
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "equalTo") {
|
|
2790
|
+
const s2 = unifyTerm(goal.s, goal.o, subst);
|
|
2791
|
+
return s2 !== null ? [s2] : [];
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// log:notEqualTo
|
|
2795
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "notEqualTo") {
|
|
2796
|
+
const s2 = unifyTerm(goal.s, goal.o, subst);
|
|
2797
|
+
if (s2 !== null) return [];
|
|
2798
|
+
return [{ ...subst }];
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// log:implies — expose internal forward rules as data
|
|
2802
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "implies") {
|
|
2803
|
+
const allFw = backRules.__allForwardRules || [];
|
|
2804
|
+
const results = [];
|
|
2805
|
+
|
|
2806
|
+
for (const r0 of allFw) {
|
|
2807
|
+
if (!r0.isForward) continue;
|
|
2808
|
+
|
|
2809
|
+
// fresh copy of the rule with fresh variable names
|
|
2810
|
+
const r = standardizeRule(r0, varGen);
|
|
2811
|
+
|
|
2812
|
+
const premF = new FormulaTerm(r.premise);
|
|
2813
|
+
const concTerm = r0.isFuse
|
|
2814
|
+
? new Literal("false")
|
|
2815
|
+
: new FormulaTerm(r.conclusion);
|
|
2816
|
+
|
|
2817
|
+
// unify subject with the premise formula
|
|
2818
|
+
let s2 = unifyTerm(goal.s, premF, subst);
|
|
2819
|
+
if (s2 === null) continue;
|
|
2820
|
+
|
|
2821
|
+
// unify object with the conclusion formula
|
|
2822
|
+
s2 = unifyTerm(goal.o, concTerm, s2);
|
|
2823
|
+
if (s2 === null) continue;
|
|
2824
|
+
|
|
2825
|
+
results.push(s2);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
return results;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
// log:impliedBy — expose internal backward rules as data
|
|
2832
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "impliedBy") {
|
|
2833
|
+
const allBw = backRules.__allBackwardRules || backRules;
|
|
2834
|
+
const results = [];
|
|
2835
|
+
|
|
2836
|
+
for (const r0 of allBw) {
|
|
2837
|
+
if (r0.isForward) continue; // only backward rules
|
|
2838
|
+
|
|
2839
|
+
// fresh copy of the rule with fresh variable names
|
|
2840
|
+
const r = standardizeRule(r0, varGen);
|
|
2841
|
+
|
|
2842
|
+
// For backward rules, r.conclusion is the head, r.premise is the body
|
|
2843
|
+
const headF = new FormulaTerm(r.conclusion);
|
|
2844
|
+
const bodyF = new FormulaTerm(r.premise);
|
|
2845
|
+
|
|
2846
|
+
// unify subject with the head formula
|
|
2847
|
+
let s2 = unifyTerm(goal.s, headF, subst);
|
|
2848
|
+
if (s2 === null) continue;
|
|
2849
|
+
|
|
2850
|
+
// unify object with the body formula
|
|
2851
|
+
s2 = unifyTerm(goal.o, bodyF, s2);
|
|
2852
|
+
if (s2 === null) continue;
|
|
2853
|
+
|
|
2854
|
+
results.push(s2);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
return results;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
// log:notIncludes
|
|
2861
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "notIncludes") {
|
|
2862
|
+
if (!(g.o instanceof FormulaTerm)) return [];
|
|
2863
|
+
const body = g.o.triples;
|
|
2864
|
+
const visited2 = [];
|
|
2865
|
+
const sols = proveGoals(
|
|
2866
|
+
Array.from(body),
|
|
2867
|
+
{},
|
|
2868
|
+
facts,
|
|
2869
|
+
backRules,
|
|
2870
|
+
depth + 1,
|
|
2871
|
+
visited2,
|
|
2872
|
+
varGen
|
|
2873
|
+
);
|
|
2874
|
+
if (!sols.length) return [{ ...subst }];
|
|
2875
|
+
return [];
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// log:collectAllIn
|
|
2879
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "collectAllIn") {
|
|
2880
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
|
|
2881
|
+
const [valueTempl, clauseTerm, listTerm] = g.s.elems;
|
|
2882
|
+
if (!(clauseTerm instanceof FormulaTerm)) return [];
|
|
2883
|
+
const body = clauseTerm.triples;
|
|
2884
|
+
const visited2 = [];
|
|
2885
|
+
const sols = proveGoals(
|
|
2886
|
+
Array.from(body),
|
|
2887
|
+
{},
|
|
2888
|
+
facts,
|
|
2889
|
+
backRules,
|
|
2890
|
+
depth + 1,
|
|
2891
|
+
visited2,
|
|
2892
|
+
varGen
|
|
2893
|
+
);
|
|
2894
|
+
|
|
2895
|
+
// Collect one value per *solution*, duplicates allowed
|
|
2896
|
+
const collected = [];
|
|
2897
|
+
for (const sBody of sols) {
|
|
2898
|
+
const v = applySubstTerm(valueTempl, sBody);
|
|
2899
|
+
collected.push(v);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
const collectedList = new ListTerm(collected);
|
|
2903
|
+
const s2 = unifyTerm(listTerm, collectedList, subst);
|
|
2904
|
+
return s2 !== null ? [s2] : [];
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// log:forAllIn
|
|
2908
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "forAllIn") {
|
|
2909
|
+
// Subject: list with two clauses (where-clause, then-clause)
|
|
2910
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
2911
|
+
const [whereClause, thenClause] = g.s.elems;
|
|
2912
|
+
if (!(whereClause instanceof FormulaTerm)) return [];
|
|
2913
|
+
if (!(thenClause instanceof FormulaTerm)) return [];
|
|
2914
|
+
|
|
2915
|
+
// 1. Find all substitutions that make the first clause true
|
|
2916
|
+
const visited1 = [];
|
|
2917
|
+
const sols1 = proveGoals(
|
|
2918
|
+
Array.from(whereClause.triples),
|
|
2919
|
+
{},
|
|
2920
|
+
facts,
|
|
2921
|
+
backRules,
|
|
2922
|
+
depth + 1,
|
|
2923
|
+
visited1,
|
|
2924
|
+
varGen
|
|
2925
|
+
);
|
|
2926
|
+
|
|
2927
|
+
// 2. For every such substitution, check that the second clause holds too.
|
|
2928
|
+
// If there are no matches for the first clause, this is vacuously true.
|
|
2929
|
+
for (const s1 of sols1) {
|
|
2930
|
+
const visited2 = [];
|
|
2931
|
+
const sols2 = proveGoals(
|
|
2932
|
+
Array.from(thenClause.triples),
|
|
2933
|
+
s1,
|
|
2934
|
+
facts,
|
|
2935
|
+
backRules,
|
|
2936
|
+
depth + 1,
|
|
2937
|
+
visited2,
|
|
2938
|
+
varGen
|
|
2939
|
+
);
|
|
2940
|
+
// Found a counterexample: whereClause holds but thenClause does not
|
|
2941
|
+
if (!sols2.length) return [];
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// All matches pass (or there were no matches) → builtin succeeds as a pure test.
|
|
2945
|
+
return [{ ...subst }];
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// log:skolem
|
|
2949
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "skolem") {
|
|
2950
|
+
// Subject must be ground; commonly a list, but we allow any ground term.
|
|
2951
|
+
if (!isGroundTerm(g.s)) return [];
|
|
2952
|
+
|
|
2953
|
+
const key = skolemKeyFromTerm(g.s);
|
|
2954
|
+
let iri = skolemCache.get(key);
|
|
2955
|
+
if (!iri) {
|
|
2956
|
+
const id = deterministicSkolemIdFromKey(key);
|
|
2957
|
+
iri = new Iri(SKOLEM_NS + id);
|
|
2958
|
+
skolemCache.set(key, iri);
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
const s2 = unifyTerm(goal.o, iri, subst);
|
|
2962
|
+
return s2 !== null ? [s2] : [];
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// log:uri
|
|
2966
|
+
if (g.p instanceof Iri && g.p.value === LOG_NS + "uri") {
|
|
2967
|
+
// Direction 1: subject is an IRI -> object is its string representation
|
|
2968
|
+
if (g.s instanceof Iri) {
|
|
2969
|
+
const uriStr = g.s.value; // raw IRI string, e.g. "https://www.w3.org"
|
|
2970
|
+
const lit = makeStringLiteral(uriStr); // "https://www.w3.org"
|
|
2971
|
+
const s2 = unifyTerm(goal.o, lit, subst);
|
|
2972
|
+
return s2 !== null ? [s2] : [];
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// Direction 2: object is a string literal -> subject is the corresponding IRI
|
|
2976
|
+
if (g.o instanceof Literal) {
|
|
2977
|
+
const uriStr = termToJsString(g.o); // JS string from the literal
|
|
2978
|
+
if (uriStr === null) return [];
|
|
2979
|
+
const iri = new Iri(uriStr);
|
|
2980
|
+
const s2 = unifyTerm(goal.s, iri, subst);
|
|
2981
|
+
return s2 !== null ? [s2] : [];
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
// If neither side is sufficiently instantiated (both vars, or wrong types),
|
|
2985
|
+
// we don't enumerate URIs; the builtin just fails.
|
|
2986
|
+
return [];
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
// -----------------------------------------------------------------
|
|
2990
|
+
// 4.6 string: builtins
|
|
2991
|
+
// -----------------------------------------------------------------
|
|
2992
|
+
|
|
2993
|
+
// string:concatenation
|
|
2994
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "concatenation") {
|
|
2995
|
+
if (!(g.s instanceof ListTerm)) return [];
|
|
2996
|
+
const parts = [];
|
|
2997
|
+
for (const t of g.s.elems) {
|
|
2998
|
+
const sStr = termToJsString(t);
|
|
2999
|
+
if (sStr === null) return [];
|
|
3000
|
+
parts.push(sStr);
|
|
3001
|
+
}
|
|
3002
|
+
const lit = makeStringLiteral(parts.join(""));
|
|
3003
|
+
|
|
3004
|
+
if (g.o instanceof Var) {
|
|
3005
|
+
const s2 = { ...subst };
|
|
3006
|
+
s2[g.o.name] = lit;
|
|
3007
|
+
return [s2];
|
|
3008
|
+
}
|
|
3009
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
3010
|
+
return s2 !== null ? [s2] : [];
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// string:contains
|
|
3014
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "contains") {
|
|
3015
|
+
const sStr = termToJsString(g.s);
|
|
3016
|
+
const oStr = termToJsString(g.o);
|
|
3017
|
+
if (sStr === null || oStr === null) return [];
|
|
3018
|
+
return sStr.includes(oStr) ? [{ ...subst }] : [];
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
// string:containsIgnoringCase
|
|
3022
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "containsIgnoringCase") {
|
|
3023
|
+
const sStr = termToJsString(g.s);
|
|
3024
|
+
const oStr = termToJsString(g.o);
|
|
3025
|
+
if (sStr === null || oStr === null) return [];
|
|
3026
|
+
return sStr.toLowerCase().includes(oStr.toLowerCase())
|
|
3027
|
+
? [{ ...subst }]
|
|
3028
|
+
: [];
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// string:endsWith
|
|
3032
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "endsWith") {
|
|
3033
|
+
const sStr = termToJsString(g.s);
|
|
3034
|
+
const oStr = termToJsString(g.o);
|
|
3035
|
+
if (sStr === null || oStr === null) return [];
|
|
3036
|
+
return sStr.endsWith(oStr) ? [{ ...subst }] : [];
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// string:equalIgnoringCase
|
|
3040
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "equalIgnoringCase") {
|
|
3041
|
+
const sStr = termToJsString(g.s);
|
|
3042
|
+
const oStr = termToJsString(g.o);
|
|
3043
|
+
if (sStr === null || oStr === null) return [];
|
|
3044
|
+
return sStr.toLowerCase() === oStr.toLowerCase()
|
|
3045
|
+
? [{ ...subst }]
|
|
3046
|
+
: [];
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// string:format
|
|
3050
|
+
// (limited: only %s and %% are supported, anything else ⇒ builtin fails)
|
|
3051
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "format") {
|
|
3052
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length < 1) return [];
|
|
3053
|
+
const fmtStr = termToJsString(g.s.elems[0]);
|
|
3054
|
+
if (fmtStr === null) return [];
|
|
3055
|
+
|
|
3056
|
+
const args = [];
|
|
3057
|
+
for (let i = 1; i < g.s.elems.length; i++) {
|
|
3058
|
+
const aStr = termToJsString(g.s.elems[i]);
|
|
3059
|
+
if (aStr === null) return [];
|
|
3060
|
+
args.push(aStr);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
const formatted = simpleStringFormat(fmtStr, args);
|
|
3064
|
+
if (formatted === null) return []; // unsupported format specifier(s)
|
|
3065
|
+
|
|
3066
|
+
const lit = makeStringLiteral(formatted);
|
|
3067
|
+
if (g.o instanceof Var) {
|
|
3068
|
+
const s2 = { ...subst };
|
|
3069
|
+
s2[g.o.name] = lit;
|
|
3070
|
+
return [s2];
|
|
3071
|
+
}
|
|
3072
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
3073
|
+
return s2 !== null ? [s2] : [];
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// string:greaterThan
|
|
3077
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "greaterThan") {
|
|
3078
|
+
const sStr = termToJsString(g.s);
|
|
3079
|
+
const oStr = termToJsString(g.o);
|
|
3080
|
+
if (sStr === null || oStr === null) return [];
|
|
3081
|
+
return sStr > oStr ? [{ ...subst }] : [];
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// string:lessThan
|
|
3085
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "lessThan") {
|
|
3086
|
+
const sStr = termToJsString(g.s);
|
|
3087
|
+
const oStr = termToJsString(g.o);
|
|
3088
|
+
if (sStr === null || oStr === null) return [];
|
|
3089
|
+
return sStr < oStr ? [{ ...subst }] : [];
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
// string:matches
|
|
3093
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "matches") {
|
|
3094
|
+
const sStr = termToJsString(g.s);
|
|
3095
|
+
const pattern = termToJsString(g.o);
|
|
3096
|
+
if (sStr === null || pattern === null) return [];
|
|
3097
|
+
let re;
|
|
3098
|
+
try {
|
|
3099
|
+
// Perl/Python-style in the spec; JS RegExp is close enough for most patterns.
|
|
3100
|
+
re = new RegExp(pattern);
|
|
3101
|
+
} catch (e) {
|
|
3102
|
+
return [];
|
|
3103
|
+
}
|
|
3104
|
+
return re.test(sStr) ? [{ ...subst }] : [];
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// string:notEqualIgnoringCase
|
|
3108
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "notEqualIgnoringCase") {
|
|
3109
|
+
const sStr = termToJsString(g.s);
|
|
3110
|
+
const oStr = termToJsString(g.o);
|
|
3111
|
+
if (sStr === null || oStr === null) return [];
|
|
3112
|
+
return sStr.toLowerCase() !== oStr.toLowerCase()
|
|
3113
|
+
? [{ ...subst }]
|
|
3114
|
+
: [];
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// string:notGreaterThan (≤ in Unicode code order)
|
|
3118
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "notGreaterThan") {
|
|
3119
|
+
const sStr = termToJsString(g.s);
|
|
3120
|
+
const oStr = termToJsString(g.o);
|
|
3121
|
+
if (sStr === null || oStr === null) return [];
|
|
3122
|
+
return sStr <= oStr ? [{ ...subst }] : [];
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// string:notLessThan (≥ in Unicode code order)
|
|
3126
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "notLessThan") {
|
|
3127
|
+
const sStr = termToJsString(g.s);
|
|
3128
|
+
const oStr = termToJsString(g.o);
|
|
3129
|
+
if (sStr === null || oStr === null) return [];
|
|
3130
|
+
return sStr >= oStr ? [{ ...subst }] : [];
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
// string:notMatches
|
|
3134
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "notMatches") {
|
|
3135
|
+
const sStr = termToJsString(g.s);
|
|
3136
|
+
const pattern = termToJsString(g.o);
|
|
3137
|
+
if (sStr === null || pattern === null) return [];
|
|
3138
|
+
let re;
|
|
3139
|
+
try {
|
|
3140
|
+
re = new RegExp(pattern);
|
|
3141
|
+
} catch (e) {
|
|
3142
|
+
return [];
|
|
3143
|
+
}
|
|
3144
|
+
return re.test(sStr) ? [] : [{ ...subst }];
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
// string:replace
|
|
3148
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "replace") {
|
|
3149
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
|
|
3150
|
+
const dataStr = termToJsString(g.s.elems[0]);
|
|
3151
|
+
const searchStr = termToJsString(g.s.elems[1]);
|
|
3152
|
+
const replStr = termToJsString(g.s.elems[2]);
|
|
3153
|
+
if (dataStr === null || searchStr === null || replStr === null) return [];
|
|
3154
|
+
|
|
3155
|
+
let re;
|
|
3156
|
+
try {
|
|
3157
|
+
// Global replacement
|
|
3158
|
+
re = new RegExp(searchStr, "g");
|
|
3159
|
+
} catch (e) {
|
|
3160
|
+
return [];
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
const outStr = dataStr.replace(re, replStr);
|
|
3164
|
+
const lit = makeStringLiteral(outStr);
|
|
3165
|
+
|
|
3166
|
+
if (g.o instanceof Var) {
|
|
3167
|
+
const s2 = { ...subst };
|
|
3168
|
+
s2[g.o.name] = lit;
|
|
3169
|
+
return [s2];
|
|
3170
|
+
}
|
|
3171
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
3172
|
+
return s2 !== null ? [s2] : [];
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// string:scrape
|
|
3176
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "scrape") {
|
|
3177
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
3178
|
+
const dataStr = termToJsString(g.s.elems[0]);
|
|
3179
|
+
const pattern = termToJsString(g.s.elems[1]);
|
|
3180
|
+
if (dataStr === null || pattern === null) return [];
|
|
3181
|
+
|
|
3182
|
+
let re;
|
|
3183
|
+
try {
|
|
3184
|
+
re = new RegExp(pattern);
|
|
3185
|
+
} catch (e) {
|
|
3186
|
+
return [];
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
const m = re.exec(dataStr);
|
|
3190
|
+
// Spec says “exactly 1 group”; we just use the first capturing group if present.
|
|
3191
|
+
if (!m || m.length < 2) return [];
|
|
3192
|
+
const group = m[1];
|
|
3193
|
+
const lit = makeStringLiteral(group);
|
|
3194
|
+
|
|
3195
|
+
if (g.o instanceof Var) {
|
|
3196
|
+
const s2 = { ...subst };
|
|
3197
|
+
s2[g.o.name] = lit;
|
|
3198
|
+
return [s2];
|
|
3199
|
+
}
|
|
3200
|
+
const s2 = unifyTerm(g.o, lit, subst);
|
|
3201
|
+
return s2 !== null ? [s2] : [];
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
// string:startsWith
|
|
3205
|
+
if (g.p instanceof Iri && g.p.value === STRING_NS + "startsWith") {
|
|
3206
|
+
const sStr = termToJsString(g.s);
|
|
3207
|
+
const oStr = termToJsString(g.o);
|
|
3208
|
+
if (sStr === null || oStr === null) return [];
|
|
3209
|
+
return sStr.startsWith(oStr) ? [{ ...subst }] : [];
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// Unknown builtin
|
|
3213
|
+
return [];
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function isBuiltinPred(p) {
|
|
3217
|
+
if (!(p instanceof Iri)) return false;
|
|
3218
|
+
const v = p.value;
|
|
3219
|
+
return (
|
|
3220
|
+
v.startsWith(CRYPTO_NS) ||
|
|
3221
|
+
v.startsWith(MATH_NS) ||
|
|
3222
|
+
v.startsWith(LOG_NS) ||
|
|
3223
|
+
v.startsWith(STRING_NS) ||
|
|
3224
|
+
v.startsWith(TIME_NS) ||
|
|
3225
|
+
v.startsWith(LIST_NS)
|
|
3226
|
+
);
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
// ============================================================================
|
|
3230
|
+
// Backward proof (SLD-style)
|
|
3231
|
+
// ============================================================================
|
|
3232
|
+
|
|
3233
|
+
function standardizeRule(rule, gen) {
|
|
3234
|
+
function renameTerm(t, vmap, genArr) {
|
|
3235
|
+
if (t instanceof Var) {
|
|
3236
|
+
if (!vmap.hasOwnProperty(t.name)) {
|
|
3237
|
+
const name = `${t.name}__${genArr[0]}`;
|
|
3238
|
+
genArr[0] += 1;
|
|
3239
|
+
vmap[t.name] = name;
|
|
3240
|
+
}
|
|
3241
|
+
return new Var(vmap[t.name]);
|
|
3242
|
+
}
|
|
3243
|
+
if (t instanceof ListTerm) {
|
|
3244
|
+
return new ListTerm(t.elems.map(e => renameTerm(e, vmap, genArr)));
|
|
3245
|
+
}
|
|
3246
|
+
if (t instanceof OpenListTerm) {
|
|
3247
|
+
const newXs = t.prefix.map(e => renameTerm(e, vmap, genArr));
|
|
3248
|
+
if (!vmap.hasOwnProperty(t.tailVar)) {
|
|
3249
|
+
const name = `${t.tailVar}__${genArr[0]}`;
|
|
3250
|
+
genArr[0] += 1;
|
|
3251
|
+
vmap[t.tailVar] = name;
|
|
3252
|
+
}
|
|
3253
|
+
const newTail = vmap[t.tailVar];
|
|
3254
|
+
return new OpenListTerm(newXs, newTail);
|
|
3255
|
+
}
|
|
3256
|
+
if (t instanceof FormulaTerm) {
|
|
3257
|
+
return new FormulaTerm(
|
|
3258
|
+
t.triples.map(tr =>
|
|
3259
|
+
new Triple(
|
|
3260
|
+
renameTerm(tr.s, vmap, genArr),
|
|
3261
|
+
renameTerm(tr.p, vmap, genArr),
|
|
3262
|
+
renameTerm(tr.o, vmap, genArr)
|
|
3263
|
+
)
|
|
3264
|
+
)
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
3267
|
+
return t;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
const vmap2 = {};
|
|
3271
|
+
const premise = rule.premise.map(
|
|
3272
|
+
tr =>
|
|
3273
|
+
new Triple(
|
|
3274
|
+
renameTerm(tr.s, vmap2, gen),
|
|
3275
|
+
renameTerm(tr.p, vmap2, gen),
|
|
3276
|
+
renameTerm(tr.o, vmap2, gen)
|
|
3277
|
+
)
|
|
3278
|
+
);
|
|
3279
|
+
const conclusion = rule.conclusion.map(
|
|
3280
|
+
tr =>
|
|
3281
|
+
new Triple(
|
|
3282
|
+
renameTerm(tr.s, vmap2, gen),
|
|
3283
|
+
renameTerm(tr.p, vmap2, gen),
|
|
3284
|
+
renameTerm(tr.o, vmap2, gen)
|
|
3285
|
+
)
|
|
3286
|
+
);
|
|
3287
|
+
return new Rule(
|
|
3288
|
+
premise,
|
|
3289
|
+
conclusion,
|
|
3290
|
+
rule.isForward,
|
|
3291
|
+
rule.isFuse,
|
|
3292
|
+
rule.headBlankLabels
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
function listHasTriple(list, tr) {
|
|
3297
|
+
return list.some(t => triplesEqual(t, tr));
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// ============================================================================
|
|
3301
|
+
// Substitution compaction (to avoid O(depth^2) in deep backward chains)
|
|
3302
|
+
// ============================================================================
|
|
3303
|
+
//
|
|
3304
|
+
// Why: backward chaining with standardizeRule introduces fresh variables at
|
|
3305
|
+
// each step. composeSubst frequently copies a growing substitution object.
|
|
3306
|
+
// For deep linear recursions this becomes quadratic.
|
|
3307
|
+
//
|
|
3308
|
+
// Strategy: when the substitution is "large" or search depth is high,
|
|
3309
|
+
// keep only bindings that are still relevant to:
|
|
3310
|
+
// - variables appearing in the remaining goals
|
|
3311
|
+
// - variables from the original goals (answer vars)
|
|
3312
|
+
// plus the transitive closure of variables that appear inside kept bindings.
|
|
3313
|
+
//
|
|
3314
|
+
// This is semantics-preserving for the ongoing proof state.
|
|
3315
|
+
|
|
3316
|
+
function _gcCollectVarsInTerm(t, out) {
|
|
3317
|
+
if (t instanceof Var) { out.add(t.name); return; }
|
|
3318
|
+
if (t instanceof ListTerm) { for (const e of t.elems) _gcCollectVarsInTerm(e, out); return; }
|
|
3319
|
+
if (t instanceof OpenListTerm) {
|
|
3320
|
+
for (const e of t.prefix) _gcCollectVarsInTerm(e, out);
|
|
3321
|
+
out.add(t.tailVar);
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (t instanceof FormulaTerm) { for (const tr of t.triples) _gcCollectVarsInTriple(tr, out); return; }
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
function _gcCollectVarsInTriple(tr, out) {
|
|
3328
|
+
_gcCollectVarsInTerm(tr.s, out);
|
|
3329
|
+
_gcCollectVarsInTerm(tr.p, out);
|
|
3330
|
+
_gcCollectVarsInTerm(tr.o, out);
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
function _gcCollectVarsInGoals(goals, out) {
|
|
3334
|
+
for (const g of goals) _gcCollectVarsInTriple(g, out);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function _substSizeOver(subst, limit) {
|
|
3338
|
+
let c = 0;
|
|
3339
|
+
for (const _k in subst) {
|
|
3340
|
+
if (++c > limit) return true;
|
|
3341
|
+
}
|
|
3342
|
+
return false;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function _gcCompactForGoals(subst, goals, answerVars) {
|
|
3346
|
+
const keep = new Set(answerVars);
|
|
3347
|
+
_gcCollectVarsInGoals(goals, keep);
|
|
3348
|
+
|
|
3349
|
+
const expanded = new Set();
|
|
3350
|
+
const queue = Array.from(keep);
|
|
3351
|
+
|
|
3352
|
+
while (queue.length) {
|
|
3353
|
+
const v = queue.pop();
|
|
3354
|
+
if (expanded.has(v)) continue;
|
|
3355
|
+
expanded.add(v);
|
|
3356
|
+
|
|
3357
|
+
const bound = subst[v];
|
|
3358
|
+
if (bound === undefined) continue;
|
|
3359
|
+
|
|
3360
|
+
const before = keep.size;
|
|
3361
|
+
_gcCollectVarsInTerm(bound, keep);
|
|
3362
|
+
if (keep.size !== before) {
|
|
3363
|
+
for (const nv of keep) {
|
|
3364
|
+
if (!expanded.has(nv)) queue.push(nv);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
const out = {};
|
|
3370
|
+
for (const k of Object.keys(subst)) {
|
|
3371
|
+
if (keep.has(k)) out[k] = subst[k];
|
|
3372
|
+
}
|
|
3373
|
+
return out;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
function _maybeCompactSubst(subst, goals, answerVars, depth) {
|
|
3377
|
+
// Keep the fast path fast.
|
|
3378
|
+
// Only compact when the substitution is clearly getting large, or
|
|
3379
|
+
// we are in a deep chain (where the quadratic behavior shows up).
|
|
3380
|
+
if (depth < 128 && !_substSizeOver(subst, 256)) return subst;
|
|
3381
|
+
return _gcCompactForGoals(subst, goals, answerVars);
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
|
|
3385
|
+
function proveGoals( goals, subst, facts, backRules, depth, visited, varGen ) {
|
|
3386
|
+
// Iterative DFS over proof states using an explicit stack.
|
|
3387
|
+
// Each state carries its own substitution and remaining goals.
|
|
3388
|
+
const results = [];
|
|
3389
|
+
|
|
3390
|
+
const initialGoals = Array.isArray(goals) ? goals.slice() : [];
|
|
3391
|
+
const initialSubst = subst ? { ...subst } : {};
|
|
3392
|
+
const initialVisited = visited ? visited.slice() : [];
|
|
3393
|
+
|
|
3394
|
+
|
|
3395
|
+
// Variables from the original goal list (needed by the caller to instantiate conclusions)
|
|
3396
|
+
const answerVars = new Set();
|
|
3397
|
+
_gcCollectVarsInGoals(initialGoals, answerVars);
|
|
3398
|
+
if (!initialGoals.length) {
|
|
3399
|
+
results.push(_gcCompactForGoals(initialSubst, [], answerVars));
|
|
3400
|
+
return results;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
const stack = [
|
|
3404
|
+
{ goals: initialGoals, subst: initialSubst, depth: depth || 0, visited: initialVisited }
|
|
3405
|
+
];
|
|
3406
|
+
|
|
3407
|
+
while (stack.length) {
|
|
3408
|
+
const state = stack.pop();
|
|
3409
|
+
|
|
3410
|
+
if (!state.goals.length) {
|
|
3411
|
+
results.push(_gcCompactForGoals(state.subst, [], answerVars));
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
const rawGoal = state.goals[0];
|
|
3416
|
+
const restGoals = state.goals.slice(1);
|
|
3417
|
+
const goal0 = applySubstTriple(rawGoal, state.subst);
|
|
3418
|
+
|
|
3419
|
+
// 1) Builtins
|
|
3420
|
+
if (isBuiltinPred(goal0.p)) {
|
|
3421
|
+
const deltas = evalBuiltin(goal0, {}, facts, backRules, state.depth, varGen);
|
|
3422
|
+
for (const delta of deltas) {
|
|
3423
|
+
const composed = composeSubst(state.subst, delta);
|
|
3424
|
+
if (composed === null) continue;
|
|
3425
|
+
|
|
3426
|
+
if (!restGoals.length) {
|
|
3427
|
+
results.push(_gcCompactForGoals(composed, [], answerVars));
|
|
3428
|
+
} else {
|
|
3429
|
+
const nextSubst = _maybeCompactSubst(composed, restGoals, answerVars, state.depth + 1);
|
|
3430
|
+
stack.push({
|
|
3431
|
+
goals: restGoals,
|
|
3432
|
+
subst: nextSubst,
|
|
3433
|
+
depth: state.depth + 1,
|
|
3434
|
+
visited: state.visited
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
continue;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// 2) Loop check for backward reasoning
|
|
3442
|
+
if (listHasTriple(state.visited, goal0)) continue;
|
|
3443
|
+
const visitedForRules = state.visited.concat([goal0]);
|
|
3444
|
+
|
|
3445
|
+
// 3) Try to satisfy the goal from known facts (NOW indexed by (p,o) when possible)
|
|
3446
|
+
if (goal0.p instanceof Iri) {
|
|
3447
|
+
const candidates = candidateFacts(facts, goal0);
|
|
3448
|
+
for (const f of candidates) {
|
|
3449
|
+
const delta = unifyTriple(goal0, f, {});
|
|
3450
|
+
if (delta === null) continue;
|
|
3451
|
+
|
|
3452
|
+
const composed = composeSubst(state.subst, delta);
|
|
3453
|
+
if (composed === null) continue;
|
|
3454
|
+
|
|
3455
|
+
if (!restGoals.length) {
|
|
3456
|
+
results.push(_gcCompactForGoals(composed, [], answerVars));
|
|
3457
|
+
} else {
|
|
3458
|
+
const nextSubst = _maybeCompactSubst(composed, restGoals, answerVars, state.depth + 1);
|
|
3459
|
+
stack.push({
|
|
3460
|
+
goals: restGoals,
|
|
3461
|
+
subst: nextSubst,
|
|
3462
|
+
depth: state.depth + 1,
|
|
3463
|
+
visited: state.visited
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
} else {
|
|
3468
|
+
// Non-IRI predicate → must try all facts.
|
|
3469
|
+
for (const f of facts) {
|
|
3470
|
+
const delta = unifyTriple(goal0, f, {});
|
|
3471
|
+
if (delta === null) continue;
|
|
3472
|
+
|
|
3473
|
+
const composed = composeSubst(state.subst, delta);
|
|
3474
|
+
if (composed === null) continue;
|
|
3475
|
+
|
|
3476
|
+
if (!restGoals.length) {
|
|
3477
|
+
results.push(_gcCompactForGoals(composed, [], answerVars));
|
|
3478
|
+
} else {
|
|
3479
|
+
const nextSubst = _maybeCompactSubst(composed, restGoals, answerVars, state.depth + 1);
|
|
3480
|
+
stack.push({
|
|
3481
|
+
goals: restGoals,
|
|
3482
|
+
subst: nextSubst,
|
|
3483
|
+
depth: state.depth + 1,
|
|
3484
|
+
visited: state.visited
|
|
3485
|
+
});
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// 4) Backward rules (indexed by head predicate)
|
|
3491
|
+
if (goal0.p instanceof Iri) {
|
|
3492
|
+
ensureBackRuleIndexes(backRules);
|
|
3493
|
+
const candRules =
|
|
3494
|
+
(backRules.__byHeadPred.get(goal0.p.value) || []).concat(backRules.__wildHeadPred);
|
|
3495
|
+
|
|
3496
|
+
for (const r of candRules) {
|
|
3497
|
+
if (r.conclusion.length !== 1) continue;
|
|
3498
|
+
|
|
3499
|
+
const rawHead = r.conclusion[0];
|
|
3500
|
+
if (rawHead.p instanceof Iri && rawHead.p.value !== goal0.p.value) continue;
|
|
3501
|
+
|
|
3502
|
+
const rStd = standardizeRule(r, varGen);
|
|
3503
|
+
const head = rStd.conclusion[0];
|
|
3504
|
+
const deltaHead = unifyTriple(head, goal0, {});
|
|
3505
|
+
if (deltaHead === null) continue;
|
|
3506
|
+
|
|
3507
|
+
const body = rStd.premise.map(b => applySubstTriple(b, deltaHead));
|
|
3508
|
+
const composed = composeSubst(state.subst, deltaHead);
|
|
3509
|
+
if (composed === null) continue;
|
|
3510
|
+
|
|
3511
|
+
const newGoals = body.concat(restGoals);
|
|
3512
|
+
const nextSubst = _maybeCompactSubst(composed, newGoals, answerVars, state.depth + 1);
|
|
3513
|
+
stack.push({
|
|
3514
|
+
goals: newGoals,
|
|
3515
|
+
subst: nextSubst,
|
|
3516
|
+
depth: state.depth + 1,
|
|
3517
|
+
visited: visitedForRules
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
return results;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// ============================================================================
|
|
3527
|
+
// Forward chaining to fixpoint
|
|
3528
|
+
// ============================================================================
|
|
3529
|
+
|
|
3530
|
+
function forwardChain(facts, forwardRules, backRules) {
|
|
3531
|
+
ensureFactIndexes(facts);
|
|
3532
|
+
ensureBackRuleIndexes(backRules);
|
|
3533
|
+
|
|
3534
|
+
const factList = facts.slice();
|
|
3535
|
+
const derivedForward = [];
|
|
3536
|
+
const varGen = [0];
|
|
3537
|
+
const skCounter = [0];
|
|
3538
|
+
|
|
3539
|
+
// Make rules visible to introspection builtins
|
|
3540
|
+
backRules.__allForwardRules = forwardRules;
|
|
3541
|
+
backRules.__allBackwardRules = backRules;
|
|
3542
|
+
|
|
3543
|
+
while (true) {
|
|
3544
|
+
let changed = false;
|
|
3545
|
+
|
|
3546
|
+
for (let i = 0; i < forwardRules.length; i++) {
|
|
3547
|
+
const r = forwardRules[i];
|
|
3548
|
+
const empty = {};
|
|
3549
|
+
const visited = [];
|
|
3550
|
+
|
|
3551
|
+
const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen);
|
|
3552
|
+
|
|
3553
|
+
// Inference fuse
|
|
3554
|
+
if (r.isFuse && sols.length) {
|
|
3555
|
+
console.log("# Inference fuse triggered: a { ... } => false. rule fired.");
|
|
3556
|
+
process.exit(2);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
for (const s of sols) {
|
|
3560
|
+
const instantiatedPremises = r.premise.map(b => applySubstTriple(b, s));
|
|
3561
|
+
|
|
3562
|
+
for (const cpat of r.conclusion) {
|
|
3563
|
+
const instantiated = applySubstTriple(cpat, s);
|
|
3564
|
+
|
|
3565
|
+
const isFwRuleTriple =
|
|
3566
|
+
isLogImplies(instantiated.p) &&
|
|
3567
|
+
(
|
|
3568
|
+
(instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
|
|
3569
|
+
(instantiated.s instanceof Literal && instantiated.s.value === "true" && instantiated.o instanceof FormulaTerm) ||
|
|
3570
|
+
(instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === "true")
|
|
3571
|
+
);
|
|
3572
|
+
|
|
3573
|
+
const isBwRuleTriple =
|
|
3574
|
+
isLogImpliedBy(instantiated.p) &&
|
|
3575
|
+
(
|
|
3576
|
+
(instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
|
|
3577
|
+
(instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === "true") ||
|
|
3578
|
+
(instantiated.s instanceof Literal && instantiated.s.value === "true" && instantiated.o instanceof FormulaTerm)
|
|
3579
|
+
);
|
|
3580
|
+
|
|
3581
|
+
if (isFwRuleTriple || isBwRuleTriple) {
|
|
3582
|
+
if (!hasFactIndexed(facts, instantiated)) {
|
|
3583
|
+
factList.push(instantiated);
|
|
3584
|
+
pushFactIndexed(facts, instantiated);
|
|
3585
|
+
derivedForward.push(new DerivedFact(instantiated, r, instantiatedPremises.slice(), { ...s }));
|
|
3586
|
+
changed = true;
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
// Promote rule-producing triples to live rules, treating literal true as {}.
|
|
3590
|
+
const left =
|
|
3591
|
+
instantiated.s instanceof FormulaTerm ? instantiated.s.triples :
|
|
3592
|
+
(instantiated.s instanceof Literal && instantiated.s.value === "true") ? [] :
|
|
3593
|
+
null;
|
|
3594
|
+
|
|
3595
|
+
const right =
|
|
3596
|
+
instantiated.o instanceof FormulaTerm ? instantiated.o.triples :
|
|
3597
|
+
(instantiated.o instanceof Literal && instantiated.o.value === "true") ? [] :
|
|
3598
|
+
null;
|
|
3599
|
+
|
|
3600
|
+
if (left !== null && right !== null) {
|
|
3601
|
+
if (isFwRuleTriple) {
|
|
3602
|
+
const [premise0, conclusion] = liftBlankRuleVars(left, right);
|
|
3603
|
+
const premise = reorderPremiseForConstraints(premise0);
|
|
3604
|
+
|
|
3605
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
3606
|
+
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
3607
|
+
|
|
3608
|
+
const already = forwardRules.some(
|
|
3609
|
+
rr =>
|
|
3610
|
+
rr.isForward === newRule.isForward &&
|
|
3611
|
+
rr.isFuse === newRule.isFuse &&
|
|
3612
|
+
triplesListEqual(rr.premise, newRule.premise) &&
|
|
3613
|
+
triplesListEqual(rr.conclusion, newRule.conclusion)
|
|
3614
|
+
);
|
|
3615
|
+
if (!already) forwardRules.push(newRule);
|
|
3616
|
+
} else if (isBwRuleTriple) {
|
|
3617
|
+
const [premise, conclusion] = liftBlankRuleVars(right, left);
|
|
3618
|
+
|
|
3619
|
+
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
3620
|
+
const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
|
|
3621
|
+
|
|
3622
|
+
const already = backRules.some(
|
|
3623
|
+
rr =>
|
|
3624
|
+
rr.isForward === newRule.isForward &&
|
|
3625
|
+
rr.isFuse === newRule.isFuse &&
|
|
3626
|
+
triplesListEqual(rr.premise, newRule.premise) &&
|
|
3627
|
+
triplesListEqual(rr.conclusion, newRule.conclusion)
|
|
3628
|
+
);
|
|
3629
|
+
if (!already) {
|
|
3630
|
+
backRules.push(newRule);
|
|
3631
|
+
indexBackRule(backRules, newRule);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
continue; // skip normal fact handling
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// Only skolemize blank nodes that occur explicitly in the rule head
|
|
3640
|
+
const skMap = {};
|
|
3641
|
+
const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter);
|
|
3642
|
+
|
|
3643
|
+
if (!isGroundTriple(inst)) continue;
|
|
3644
|
+
if (hasFactIndexed(facts, inst)) continue;
|
|
3645
|
+
|
|
3646
|
+
factList.push(inst);
|
|
3647
|
+
pushFactIndexed(facts, inst);
|
|
3648
|
+
|
|
3649
|
+
derivedForward.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
|
|
3650
|
+
changed = true;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
if (!changed) break;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
return derivedForward;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
// ============================================================================
|
|
3662
|
+
// Pretty printing as N3/Turtle
|
|
3663
|
+
// ============================================================================
|
|
3664
|
+
|
|
3665
|
+
function termToN3(t, pref) {
|
|
3666
|
+
if (t instanceof Iri) {
|
|
3667
|
+
const i = t.value;
|
|
3668
|
+
const q = pref.shrinkIri(i);
|
|
3669
|
+
if (q !== null) return q;
|
|
3670
|
+
if (i.startsWith("_:")) return i;
|
|
3671
|
+
return `<${i}>`;
|
|
3672
|
+
}
|
|
3673
|
+
if (t instanceof Literal) return t.value;
|
|
3674
|
+
if (t instanceof Var) return `?${t.name}`;
|
|
3675
|
+
if (t instanceof Blank) return t.label;
|
|
3676
|
+
if (t instanceof ListTerm) {
|
|
3677
|
+
const inside = t.elems.map(e => termToN3(e, pref));
|
|
3678
|
+
return "(" + inside.join(" ") + ")";
|
|
3679
|
+
}
|
|
3680
|
+
if (t instanceof OpenListTerm) {
|
|
3681
|
+
const inside = t.prefix.map(e => termToN3(e, pref));
|
|
3682
|
+
inside.push("?" + t.tailVar);
|
|
3683
|
+
return "(" + inside.join(" ") + ")";
|
|
3684
|
+
}
|
|
3685
|
+
if (t instanceof FormulaTerm) {
|
|
3686
|
+
let s = "{\n";
|
|
3687
|
+
for (const tr of t.triples) {
|
|
3688
|
+
let line = tripleToN3(tr, pref).trimEnd();
|
|
3689
|
+
if (line) s += " " + line + "\n";
|
|
3690
|
+
}
|
|
3691
|
+
s += "}";
|
|
3692
|
+
return s;
|
|
3693
|
+
}
|
|
3694
|
+
return JSON.stringify(t);
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function tripleToN3(tr, prefixes) {
|
|
3698
|
+
// log:implies / log:impliedBy as => / <= syntactic sugar everywhere
|
|
3699
|
+
if (isLogImplies(tr.p)) {
|
|
3700
|
+
const s = termToN3(tr.s, prefixes);
|
|
3701
|
+
const o = termToN3(tr.o, prefixes);
|
|
3702
|
+
return `${s} => ${o} .`;
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
if (isLogImpliedBy(tr.p)) {
|
|
3706
|
+
const s = termToN3(tr.s, prefixes);
|
|
3707
|
+
const o = termToN3(tr.o, prefixes);
|
|
3708
|
+
return `${s} <= ${o} .`;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
const s = termToN3(tr.s, prefixes);
|
|
3712
|
+
const p =
|
|
3713
|
+
isRdfTypePred(tr.p) ? "a"
|
|
3714
|
+
: isOwlSameAsPred(tr.p) ? "="
|
|
3715
|
+
: termToN3(tr.p, prefixes);
|
|
3716
|
+
const o = termToN3(tr.o, prefixes);
|
|
3717
|
+
|
|
3718
|
+
return `${s} ${p} ${o} .`;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
function printExplanation(df, prefixes) {
|
|
3722
|
+
console.log(
|
|
3723
|
+
"# ----------------------------------------------------------------------"
|
|
3724
|
+
);
|
|
3725
|
+
console.log("# Proof for derived triple:");
|
|
3726
|
+
|
|
3727
|
+
// Fact line(s), indented 2 spaces after '# '
|
|
3728
|
+
for (const line of tripleToN3(df.fact, prefixes).split(/\r?\n/)) {
|
|
3729
|
+
const stripped = line.replace(/\s+$/, "");
|
|
3730
|
+
if (stripped) {
|
|
3731
|
+
console.log("# " + stripped);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
if (!df.premises.length) {
|
|
3736
|
+
console.log(
|
|
3737
|
+
"# This triple is the head of a forward rule with an empty premise,"
|
|
3738
|
+
);
|
|
3739
|
+
console.log(
|
|
3740
|
+
"# so it holds unconditionally whenever the program is loaded."
|
|
3741
|
+
);
|
|
3742
|
+
} else {
|
|
3743
|
+
console.log(
|
|
3744
|
+
"# It holds because the following instance of the rule body is provable:"
|
|
3745
|
+
);
|
|
3746
|
+
|
|
3747
|
+
// Premises, also indented 2 spaces after '# '
|
|
3748
|
+
for (const prem of df.premises) {
|
|
3749
|
+
for (const line of tripleToN3(prem, prefixes).split(/\r?\n/)) {
|
|
3750
|
+
const stripped = line.replace(/\s+$/, "");
|
|
3751
|
+
if (stripped) {
|
|
3752
|
+
console.log("# " + stripped);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
console.log("# via the schematic forward rule:");
|
|
3758
|
+
|
|
3759
|
+
// Rule pretty-printed
|
|
3760
|
+
console.log("# {");
|
|
3761
|
+
for (const tr of df.rule.premise) {
|
|
3762
|
+
for (const line of tripleToN3(tr, prefixes).split(/\r?\n/)) {
|
|
3763
|
+
const stripped = line.replace(/\s+$/, "");
|
|
3764
|
+
if (stripped) {
|
|
3765
|
+
console.log("# " + stripped);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
console.log("# } => {");
|
|
3770
|
+
for (const tr of df.rule.conclusion) {
|
|
3771
|
+
for (const line of tripleToN3(tr, prefixes).split(/\r?\n/)) {
|
|
3772
|
+
const stripped = line.replace(/\s+$/, "");
|
|
3773
|
+
if (stripped) {
|
|
3774
|
+
console.log("# " + stripped);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
console.log("# } .");
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// Substitution block
|
|
3782
|
+
const ruleVars = varsInRule(df.rule);
|
|
3783
|
+
const visibleNames = Object.keys(df.subst)
|
|
3784
|
+
.filter(name => ruleVars.has(name))
|
|
3785
|
+
.sort();
|
|
3786
|
+
|
|
3787
|
+
if (visibleNames.length) {
|
|
3788
|
+
console.log("# with substitution (on rule variables):");
|
|
3789
|
+
for (const v of visibleNames) {
|
|
3790
|
+
const fullTerm = applySubstTerm(new Var(v), df.subst);
|
|
3791
|
+
const rendered = termToN3(fullTerm, prefixes);
|
|
3792
|
+
const lines = rendered.split(/\r?\n/);
|
|
3793
|
+
|
|
3794
|
+
if (lines.length === 1) {
|
|
3795
|
+
// single-line term
|
|
3796
|
+
const stripped = lines[0].replace(/\s+$/, "");
|
|
3797
|
+
if (stripped) {
|
|
3798
|
+
console.log("# ?" + v + " = " + stripped);
|
|
3799
|
+
}
|
|
3800
|
+
} else {
|
|
3801
|
+
// multi-line term (e.g. a formula)
|
|
3802
|
+
const first = lines[0].trimEnd(); // usually "{"
|
|
3803
|
+
if (first) {
|
|
3804
|
+
console.log("# ?" + v + " = " + first);
|
|
3805
|
+
}
|
|
3806
|
+
for (let i = 1; i < lines.length; i++) {
|
|
3807
|
+
const stripped = lines[i].trim();
|
|
3808
|
+
if (!stripped) continue;
|
|
3809
|
+
if (i === lines.length - 1) {
|
|
3810
|
+
// closing brace
|
|
3811
|
+
console.log("# " + stripped);
|
|
3812
|
+
} else {
|
|
3813
|
+
// inner triple lines
|
|
3814
|
+
console.log("# " + stripped);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
console.log(
|
|
3822
|
+
"# Therefore the derived triple above is entailed by the rules and facts."
|
|
3823
|
+
);
|
|
3824
|
+
console.log(
|
|
3825
|
+
"# ----------------------------------------------------------------------\n"
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// ============================================================================
|
|
3830
|
+
// Misc helpers
|
|
3831
|
+
// ============================================================================
|
|
3832
|
+
|
|
3833
|
+
function localIsoDateTimeString(d) {
|
|
3834
|
+
function pad(n, width = 2) {
|
|
3835
|
+
return String(n).padStart(width, "0");
|
|
3836
|
+
}
|
|
3837
|
+
const year = d.getFullYear();
|
|
3838
|
+
const month = d.getMonth() + 1;
|
|
3839
|
+
const day = d.getDate();
|
|
3840
|
+
const hour = d.getHours();
|
|
3841
|
+
const min = d.getMinutes();
|
|
3842
|
+
const sec = d.getSeconds();
|
|
3843
|
+
const ms = d.getMilliseconds();
|
|
3844
|
+
const offsetMin = -d.getTimezoneOffset(); // minutes east of UTC
|
|
3845
|
+
const sign = offsetMin >= 0 ? "+" : "-";
|
|
3846
|
+
const abs = Math.abs(offsetMin);
|
|
3847
|
+
const oh = Math.floor(abs / 60);
|
|
3848
|
+
const om = abs % 60;
|
|
3849
|
+
const msPart = ms ? "." + String(ms).padStart(3, "0") : "";
|
|
3850
|
+
return (
|
|
3851
|
+
pad(year, 4) +
|
|
3852
|
+
"-" +
|
|
3853
|
+
pad(month) +
|
|
3854
|
+
"-" +
|
|
3855
|
+
pad(day) +
|
|
3856
|
+
"T" +
|
|
3857
|
+
pad(hour) +
|
|
3858
|
+
":" +
|
|
3859
|
+
pad(min) +
|
|
3860
|
+
":" +
|
|
3861
|
+
pad(sec) +
|
|
3862
|
+
msPart +
|
|
3863
|
+
sign +
|
|
3864
|
+
pad(oh) +
|
|
3865
|
+
":" +
|
|
3866
|
+
pad(om)
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// ============================================================================
|
|
3871
|
+
// CLI entry point
|
|
3872
|
+
// ============================================================================
|
|
3873
|
+
|
|
3874
|
+
function main() {
|
|
3875
|
+
// Drop "node" and script name; keep only user-provided args
|
|
3876
|
+
const argv = process.argv.slice(2);
|
|
3877
|
+
|
|
3878
|
+
// --------------------------------------------------------------------------
|
|
3879
|
+
// Global options
|
|
3880
|
+
// --------------------------------------------------------------------------
|
|
3881
|
+
|
|
3882
|
+
// --version / -v: print version and exit
|
|
3883
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
3884
|
+
console.log(`eyeling v${version}`);
|
|
3885
|
+
process.exit(0);
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// --no-proof-comments / -n: disable proof explanations
|
|
3889
|
+
if (argv.includes("--no-proof-comments") || argv.includes("-n")) {
|
|
3890
|
+
proofCommentsEnabled = false;
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3893
|
+
// --------------------------------------------------------------------------
|
|
3894
|
+
// Positional args (the N3 file)
|
|
3895
|
+
// --------------------------------------------------------------------------
|
|
3896
|
+
const positional = argv.filter(a => !a.startsWith("-"));
|
|
3897
|
+
|
|
3898
|
+
if (positional.length !== 1) {
|
|
3899
|
+
console.error(
|
|
3900
|
+
"Usage: eyeling.js [--version|-v] [--no-proof-comments|-n] <file.n3>"
|
|
3901
|
+
);
|
|
3902
|
+
process.exit(1);
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
const path = positional[0];
|
|
3906
|
+
let text;
|
|
3907
|
+
try {
|
|
3908
|
+
const fs = require("fs");
|
|
3909
|
+
text = fs.readFileSync(path, { encoding: "utf8" });
|
|
3910
|
+
} catch (e) {
|
|
3911
|
+
console.error(`Error reading file ${JSON.stringify(path)}: ${e.message}`);
|
|
3912
|
+
process.exit(1);
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
const toks = lex(text);
|
|
3916
|
+
const parser = new Parser(toks);
|
|
3917
|
+
const [prefixes, triples, frules, brules] = parser.parseDocument();
|
|
3918
|
+
|
|
3919
|
+
const facts = triples.filter(tr => isGroundTriple(tr));
|
|
3920
|
+
const derived = forwardChain(facts, frules, brules);
|
|
3921
|
+
|
|
3922
|
+
const derivedTriples = derived.map(df => df.fact);
|
|
3923
|
+
const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
|
|
3924
|
+
|
|
3925
|
+
for (const [pfx, base] of usedPrefixes) {
|
|
3926
|
+
if (pfx === "") console.log(`@prefix : <${base}> .`);
|
|
3927
|
+
else console.log(`@prefix ${pfx}: <${base}> .`);
|
|
3928
|
+
}
|
|
3929
|
+
if (derived.length && usedPrefixes.length) console.log();
|
|
3930
|
+
|
|
3931
|
+
for (const df of derived) {
|
|
3932
|
+
if (proofCommentsEnabled) {
|
|
3933
|
+
printExplanation(df, prefixes);
|
|
3934
|
+
console.log(tripleToN3(df.fact, prefixes));
|
|
3935
|
+
console.log();
|
|
3936
|
+
} else {
|
|
3937
|
+
console.log(tripleToN3(df.fact, prefixes));
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
if (require.main === module) {
|
|
3943
|
+
main();
|
|
3944
|
+
}
|
|
3945
|
+
|