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.
Files changed (4) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +188 -0
  3. package/eyeling.js +3945 -0
  4. 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
+