eyeling 1.5.34 → 1.5.36

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.
@@ -1,3 +1,8 @@
1
+ # =================================================
2
+ # JSON Pointer example
3
+ # See https://datatracker.ietf.org/doc/html/rfc6901
4
+ # =================================================
5
+
1
6
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2
7
  @prefix string: <http://www.w3.org/2000/10/swap/string#> .
3
8
  @prefix list: <http://www.w3.org/2000/10/swap/list#> .
@@ -1,3 +1,8 @@
1
+ # =================================================
2
+ # Rconcile VAT JSON Pointer example
3
+ # See https://datatracker.ietf.org/doc/html/rfc6901
4
+ # =================================================
5
+
1
6
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2
7
  @prefix string: <http://www.w3.org/2000/10/swap/string#> .
3
8
  @prefix list: <http://www.w3.org/2000/10/swap/list#> .
@@ -0,0 +1,31 @@
1
+ @prefix : <http://example.org/ns#> .
2
+
3
+ # ----------------------------------------------------------------------
4
+ # Proof for derived triple:
5
+ # :result :is true .
6
+ # It holds because the following instance of the rule body is provable:
7
+ # :X :val (1 2) .
8
+ # (1 2) :head 1 .
9
+ # (1 2) :tail (2) .
10
+ # 1 log:equalTo 1 .
11
+ # (2) log:equalTo (2) .
12
+ # via the schematic forward rule:
13
+ # {
14
+ # ?X :val ?Y .
15
+ # ?Y :head ?H .
16
+ # ?Y :tail ?T .
17
+ # ?H log:equalTo 1 .
18
+ # ?T log:equalTo (2) .
19
+ # } => {
20
+ # :result :is true .
21
+ # } .
22
+ # with substitution (on rule variables):
23
+ # ?H = 1
24
+ # ?T = (2)
25
+ # ?X = :X
26
+ # ?Y = (1 2)
27
+ # Therefore the derived triple above is entailed by the rules and facts.
28
+ # ----------------------------------------------------------------------
29
+
30
+ :result :is true .
31
+
@@ -0,0 +1,26 @@
1
+ # =================================
2
+ # RDF list
3
+ # Example from Patrick Hochstenbach
4
+ # =================================
5
+
6
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
7
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
8
+ @prefix : <http://example.org/ns#> .
9
+
10
+ :X :val (1 2).
11
+
12
+ { ?X :head ?Y } <= { ?X rdf:first ?Y }.
13
+ { ?X :tail ?Y } <= { ?X rdf:rest ?Y} .
14
+
15
+ {
16
+ ?X :val ?Y.
17
+ ?Y :head ?H .
18
+ ?Y :tail ?T .
19
+
20
+ ?H log:equalTo 1.
21
+ ?T log:equalTo (2).
22
+ }
23
+ =>
24
+ {
25
+ :result :is true.
26
+ }.
package/eyeling.js CHANGED
@@ -2951,6 +2951,40 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
2951
2951
  return [];
2952
2952
  }
2953
2953
 
2954
+ // rdf:first (alias of list:first)
2955
+ // Schema: $s+ rdf:first $o-
2956
+ if (g.p instanceof Iri && g.p.value === RDF_NS + "first") {
2957
+ if (!(g.s instanceof ListTerm)) return [];
2958
+ if (!g.s.elems.length) return [];
2959
+ const first = g.s.elems[0];
2960
+ const s2 = unifyTerm(g.o, first, subst);
2961
+ return s2 !== null ? [s2] : [];
2962
+ }
2963
+
2964
+ // rdf:rest (alias of list:rest)
2965
+ // Schema: $s+ rdf:rest $o-
2966
+ if (g.p instanceof Iri && g.p.value === RDF_NS + "rest") {
2967
+ // Closed list: (a b c) -> (b c)
2968
+ if (g.s instanceof ListTerm) {
2969
+ if (!g.s.elems.length) return [];
2970
+ const rest = new ListTerm(g.s.elems.slice(1));
2971
+ const s2 = unifyTerm(g.o, rest, subst);
2972
+ return s2 !== null ? [s2] : [];
2973
+ }
2974
+ // Open list: (a b ... ?T) -> (b ... ?T)
2975
+ if (g.s instanceof OpenListTerm) {
2976
+ if (!g.s.prefix.length) return [];
2977
+ if (g.s.prefix.length === 1) {
2978
+ const s2 = unifyTerm(g.o, new Var(g.s.tailVar), subst);
2979
+ return s2 !== null ? [s2] : [];
2980
+ }
2981
+ const rest = new OpenListTerm(g.s.prefix.slice(1), g.s.tailVar);
2982
+ const s2 = unifyTerm(g.o, rest, subst);
2983
+ return s2 !== null ? [s2] : [];
2984
+ }
2985
+ return [];
2986
+ }
2987
+
2954
2988
  // list:iterate
2955
2989
  // true iff $s is a list and $o is a list (index value),
2956
2990
  // where index is a valid 0-based index into $s and value is the element at that index.
@@ -3658,6 +3692,12 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3658
3692
  function isBuiltinPred(p) {
3659
3693
  if (!(p instanceof Iri)) return false;
3660
3694
  const v = p.value;
3695
+
3696
+ // Treat RDF Collections as list-term builtins too.
3697
+ if (v === RDF_NS + "first" || v === RDF_NS + "rest") {
3698
+ return true;
3699
+ }
3700
+
3661
3701
  return (
3662
3702
  v.startsWith(CRYPTO_NS) ||
3663
3703
  v.startsWith(MATH_NS) ||
@@ -4278,6 +4318,135 @@ function printExplanation(df, prefixes) {
4278
4318
  // Misc helpers
4279
4319
  // ============================================================================
4280
4320
 
4321
+ // Turn RDF Collections described with rdf:first/rdf:rest (+ rdf:nil) into ListTerm terms.
4322
+ // This mutates triples/rules in-place so list:* builtins work on RDF-serialized lists too.
4323
+ function materializeRdfLists(triples, forwardRules, backwardRules) {
4324
+ const RDF_FIRST = RDF_NS + "first";
4325
+ const RDF_REST = RDF_NS + "rest";
4326
+ const RDF_NIL = RDF_NS + "nil";
4327
+
4328
+ function nodeKey(t) {
4329
+ if (t instanceof Blank) return "B:" + t.label;
4330
+ if (t instanceof Iri) return "I:" + t.value;
4331
+ return null;
4332
+ }
4333
+
4334
+ // Collect first/rest arcs from *input triples*
4335
+ const firstMap = new Map(); // key(subject) -> Term (object)
4336
+ const restMap = new Map(); // key(subject) -> Term (object)
4337
+ for (const tr of triples) {
4338
+ if (!(tr.p instanceof Iri)) continue;
4339
+ const k = nodeKey(tr.s);
4340
+ if (!k) continue;
4341
+ if (tr.p.value === RDF_FIRST) firstMap.set(k, tr.o);
4342
+ else if (tr.p.value === RDF_REST) restMap.set(k, tr.o);
4343
+ }
4344
+ if (!firstMap.size && !restMap.size) return;
4345
+
4346
+ const cache = new Map(); // key(node) -> ListTerm
4347
+ const visiting = new Set(); // cycle guard
4348
+
4349
+ function buildListForKey(k) {
4350
+ if (cache.has(k)) return cache.get(k);
4351
+ if (visiting.has(k)) return null; // cycle => not a well-formed list
4352
+ visiting.add(k);
4353
+
4354
+ // rdf:nil => ()
4355
+ if (k === "I:" + RDF_NIL) {
4356
+ const empty = new ListTerm([]);
4357
+ cache.set(k, empty);
4358
+ visiting.delete(k);
4359
+ return empty;
4360
+ }
4361
+
4362
+ const head = firstMap.get(k);
4363
+ const tail = restMap.get(k);
4364
+ if (head === undefined || tail === undefined) {
4365
+ visiting.delete(k);
4366
+ return null; // not a full cons cell
4367
+ }
4368
+
4369
+ const headTerm = rewriteTerm(head);
4370
+
4371
+ let tailListElems = null;
4372
+ if (tail instanceof Iri && tail.value === RDF_NIL) {
4373
+ tailListElems = [];
4374
+ } else {
4375
+ const tk = nodeKey(tail);
4376
+ if (!tk) {
4377
+ visiting.delete(k);
4378
+ return null;
4379
+ }
4380
+ const tailList = buildListForKey(tk);
4381
+ if (!tailList) {
4382
+ visiting.delete(k);
4383
+ return null;
4384
+ }
4385
+ tailListElems = tailList.elems;
4386
+ }
4387
+
4388
+ const out = new ListTerm([headTerm, ...tailListElems]);
4389
+ cache.set(k, out);
4390
+ visiting.delete(k);
4391
+ return out;
4392
+ }
4393
+
4394
+ function rewriteTerm(t) {
4395
+ // Replace list nodes (Blank/Iri) by their constructed ListTerm when possible
4396
+ const k = nodeKey(t);
4397
+ if (k) {
4398
+ const built = buildListForKey(k);
4399
+ if (built) return built;
4400
+ // Also rewrite rdf:nil even if not otherwise referenced
4401
+ if (t instanceof Iri && t.value === RDF_NIL) return new ListTerm([]);
4402
+ return t;
4403
+ }
4404
+ if (t instanceof ListTerm) {
4405
+ let changed = false;
4406
+ const elems = t.elems.map(e => {
4407
+ const r = rewriteTerm(e);
4408
+ if (r !== e) changed = true;
4409
+ return r;
4410
+ });
4411
+ return changed ? new ListTerm(elems) : t;
4412
+ }
4413
+ if (t instanceof OpenListTerm) {
4414
+ let changed = false;
4415
+ const prefix = t.prefix.map(e => {
4416
+ const r = rewriteTerm(e);
4417
+ if (r !== e) changed = true;
4418
+ return r;
4419
+ });
4420
+ return changed ? new OpenListTerm(prefix, t.tailVar) : t;
4421
+ }
4422
+ if (t instanceof FormulaTerm) {
4423
+ for (const tr of t.triples) rewriteTriple(tr);
4424
+ return t;
4425
+ }
4426
+ return t;
4427
+ }
4428
+
4429
+ function rewriteTriple(tr) {
4430
+ tr.s = rewriteTerm(tr.s);
4431
+ tr.p = rewriteTerm(tr.p);
4432
+ tr.o = rewriteTerm(tr.o);
4433
+ }
4434
+
4435
+ // Pre-build all reachable list heads
4436
+ for (const k of firstMap.keys()) buildListForKey(k);
4437
+
4438
+ // Rewrite input triples + rules in-place
4439
+ for (const tr of triples) rewriteTriple(tr);
4440
+ for (const r of forwardRules) {
4441
+ for (const tr of r.premise) rewriteTriple(tr);
4442
+ for (const tr of r.conclusion) rewriteTriple(tr);
4443
+ }
4444
+ for (const r of backwardRules) {
4445
+ for (const tr of r.premise) rewriteTriple(tr);
4446
+ for (const tr of r.conclusion) rewriteTriple(tr);
4447
+ }
4448
+ }
4449
+
4281
4450
  function localIsoDateTimeString(d) {
4282
4451
  function pad(n, width = 2) {
4283
4452
  return String(n).padStart(width, "0");
@@ -4365,6 +4534,9 @@ function main() {
4365
4534
  const [prefixes, triples, frules, brules] = parser.parseDocument();
4366
4535
  // console.log(JSON.stringify([prefixes, triples, frules, brules], null, 2));
4367
4536
 
4537
+ // Build internal ListTerm values from rdf:first/rdf:rest (+ rdf:nil) input triples
4538
+ materializeRdfLists(triples, frules, brules);
4539
+
4368
4540
  const facts = triples.filter(tr => isGroundTriple(tr));
4369
4541
  const derived = forwardChain(facts, frules, brules);
4370
4542
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.5.34",
3
+ "version": "1.5.36",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -672,6 +672,36 @@ ${U('a')} <-${U('p')} ${U('b')}.`,
672
672
  new RegExp(`_:b2\\s+<${EX}q>\\s+<${EX}joe>\\s*\\.`),
673
673
  ],
674
674
  },
675
+
676
+ { name: '48 rdf:first: works on list terms (alias of list:first)',
677
+ opt: { proofComments: false }, input: ` { ( ${U('a')} ${U('b')} ${U('c')} ) rdf:first ?x. } => { ${U('s')} ${U('first')} ?x. }.
678
+ `,
679
+ expect: [new RegExp(`${EX}s>\\s+<${EX}first>\\s+<${EX}a>\\s*\\.`)],
680
+ },
681
+
682
+ { name: '49 rdf:rest: works on list terms (alias of list:rest)',
683
+ opt: { proofComments: false }, input: ` { ( ${U('a')} ${U('b')} ${U('c')} ) rdf:rest ?r. ?r rdf:first ?y. } => { ${U('s')} ${U('second')} ?y. }.
684
+ `,
685
+ expect: [new RegExp(`${EX}s>\\s+<${EX}second>\\s+<${EX}b>\\s*\\.`)],
686
+ },
687
+
688
+ { name: '50 rdf collection materialization: rdf:first/rdf:rest triples become list terms',
689
+ opt: { proofComments: false }, input: ` ${U('s')} ${U('p')} _:l1.
690
+ _:l1 rdf:first ${U('a')}.
691
+ _:l1 rdf:rest _:l2.
692
+ _:l2 rdf:first ${U('b')}.
693
+ _:l2 rdf:rest rdf:nil.
694
+
695
+ { ${U('s')} ${U('p')} ?lst. ?lst rdf:first ?x. } => { ${U('s')} ${U('q')} ?x. }.
696
+ { ${U('s')} ${U('p')} ?lst. ?lst rdf:rest ?r. ?r rdf:first ?y. } => { ${U('s')} ${U('q2')} ?y. }.
697
+ { ${U('s')} ${U('p')} ?lst. ?lst list:rest ?r. ?r list:first ?y. } => { ${U('s')} ${U('q3')} ?y. }.
698
+ `,
699
+ expect: [
700
+ new RegExp(`${EX}s>\\s+<${EX}q>\\s+<${EX}a>\\s*\\.`),
701
+ new RegExp(`${EX}s>\\s+<${EX}q2>\\s+<${EX}b>\\s*\\.`),
702
+ new RegExp(`${EX}s>\\s+<${EX}q3>\\s+<${EX}b>\\s*\\.`),
703
+ ],
704
+ },
675
705
  ];
676
706
 
677
707
  let passed = 0;