eyeleng 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,15 +3,49 @@
3
3
  [![npm version](https://img.shields.io/npm/v/eyeleng.svg)](https://www.npmjs.com/package/eyeleng)
4
4
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.20342577-blue.svg)](https://doi.org/10.5281/zenodo.20342577)
5
5
 
6
- `eyeleng` stands for **EYE Logic Engine**. It is a compact JavaScript implementation of SHACL 1.2 Rules with two rule front-ends:
6
+ The `leng` in `eyeleng` stands for **Logic Engine Next Generation**. Eyeleng's main purpose is **automatic hybrid reasoning**: it combines forward materialization with tabled backward proving and automatically chooses how rules should be evaluated.
7
+
8
+ Eyeleng is a compact JavaScript implementation of SHACL 1.2 Rules with two rule front-ends:
7
9
 
8
10
  - **SRL** — the Shape Rules Language syntax used by the SHACL 1.2 Rules draft.
9
11
  - **RDF Rules** — a Turtle/RDF syntax for rule sets.
10
12
 
11
- Eyeleng is a forward-chaining reasoner over RDF-style triples. It is deliberately small, dependency-free at runtime, readable as ordinary JavaScript, and usable from the CLI, Node.js, and the browser playground.
13
+ Eyeleng is a compact automatic hybrid reasoner over RDF-style triples. It uses forward chaining for ordinary finite materialization while its default execution mode applies conservative hybrid planning: selected function-like predicates can be proved just in time by a tabled backward prover when they are demanded by a query or another rule body. It is deliberately small, dependency-free at runtime, readable as ordinary JavaScript, and usable from the CLI, Node.js, and the browser playground.
12
14
 
13
15
  Eyeleng implements the rules/reasoning surface. It is **not** a SHACL validation engine and does not emit SHACL validation reports.
14
16
 
17
+ ## Why Eyeleng?
18
+
19
+ Eyeleng is a next-generation path for rule-based RDF reasoning: it combines a language being developed on the W3C standards track with an execution model that automatically uses both forward and backward reasoning.
20
+
21
+ The central choice is **SHACL 1.2 Rules instead of N3 as the native rule language**. SHACL 1.2 Rules is being developed by the W3C Data Shapes Working Group as a W3C Working Draft on the Recommendation track. It defines both an RDF representation of rule sets and the concise Shape Rules Language (SRL), together with `infer` and `query` operations. By contrast, the current N3 specification is a W3C Community Group Report. Community Group Reports are useful specifications, but they are not on the W3C standards track and are not W3C-endorsed standards.
22
+
23
+ That standards position does not require giving up the kinds of programs traditionally written in N3. The N3 examples translated to SRL or RDF Rules and tested with Eyeleng so far have retained their intended reasoning behavior. This is practical evidence that existing N3 rule programs can migrate to the SHACL Rules model, although Eyeleng does not parse N3 syntax directly and this is not yet a claim that every possible N3 extension has a translation.
24
+
25
+ Eyeleng also removes a choice that N3 engines commonly expose to the rule author. In EYE, Eyeling, and Eyeron, `=>` and `<=` explicitly select forward and backward rules. In Eyeleng, rules describe the logical relationship while the engine analyzes dependencies and demand. It materializes ordinary consequences forward and can prove safe, function-like predicates backward with tabling only when they are needed. This avoids exposing internal helper triples merely because they were required during a computation, while preserving forward closure where materialization is appropriate.
26
+
27
+ The related engines therefore mark stages and implementation choices rather than hard limits on what Eyeleng may replace:
28
+
29
+ | Project | Native language and runtime | Main reason to choose it |
30
+ | --- | --- | --- |
31
+ | **EYE** | N3 on SWI-Prolog | The mature, extensive N3 implementation and ecosystem |
32
+ | **Eyeling** | N3 implemented in JavaScript | Direct JavaScript and RDF-JS integration with explicit forward and backward N3 rules |
33
+ | **Eyeron** | N3 implemented in Rust, with native and WebAssembly APIs | Rust-native or WebAssembly deployment with N3 proofs and built-ins |
34
+ | **Eyeleng** | SHACL 1.2 SRL and RDF Rules implemented in JavaScript | A W3C Recommendation-track language plus automatic hybrid reasoning |
35
+
36
+ Choose Eyeleng when you want to:
37
+
38
+ - build new rule systems on SHACL 1.2 Rules rather than a Community Group language;
39
+ - migrate N3 reasoning workloads to SRL or RDF Rules;
40
+ - let the engine choose between materialization and goal-directed evaluation;
41
+ - use tabling for recursive, function-like computations without publishing their intermediate facts;
42
+ - combine stratified negation, dependency analysis, RDF 1.2 syntax, and rule execution in one compact engine;
43
+ - run the same dependency-free implementation from a CLI, Node.js, or a browser.
44
+
45
+ In short: **Eyeleng aims to carry the practical reasoning power demonstrated by the EYE family into SHACL 1.2 Rules, with automatic hybrid execution as the default rather than explicit reasoning direction as a language-level choice.**
46
+
47
+ Standards references: [SHACL 1.2 Rules](https://www.w3.org/TR/shacl12-rules/), [Notation3 Community Group](https://www.w3.org/groups/cg/n3-dev/), and [W3C document types](https://www.w3.org/standards/types/).
48
+
15
49
  ## Quick start
16
50
 
17
51
  ```sh
@@ -43,7 +77,7 @@ It derives:
43
77
 
44
78
  Open the [Playground](https://eyereasoner.github.io/eyeleng/playground) for a self-contained browser UI with URL loading, autosave, share links, diagnostics, queries, and SRL/RDF Rules syntax selection.
45
79
 
46
- ## How the reasoner works
80
+ ## How automatic hybrid reasoning works
47
81
 
48
82
  Eyeleng computes the closure of a rule set:
49
83
 
@@ -76,6 +110,10 @@ then the body matches with `?parent = :alice` and `?child = :bob`, and the head
76
110
 
77
111
  Negation is handled by stratified evaluation: rules are grouped into dependency layers, and recursion through negation is rejected so the result stays deterministic.
78
112
 
113
+ By default, run mode uses **auto-hybrid reasoning**. Ordinary rules are still materialized forward into the closure. Rules that behave like internal functions can instead be proved backward with tabling when their predicates occur as demanded body goals. For example, a Fibonacci helper predicate can be computed just in time for the public `:fib` results, without printing all intermediate helper triples. Use `--no-hybrid` or `{ hybrid: false }` to force pure forward materialization, and use `--hybrid` or `{ hybrid: true }` to force more aggressive backward orientation.
114
+
115
+ For explicit `--query` / `--query-file` use, the query body is the top-level goal. Each triple pattern in that body is then a sub-goal that may be answered by the backward prover. If pure backward proving is not safe for the demanded predicates, query mode can run a hybrid plan and finally fall back to ordinary forward closure.
116
+
79
117
  Recursive rules that create new terms through `SET`/`BIND` or head blank nodes run in relaxed mode by default. Relaxed mode can derive useful finite closures, but termination is not guaranteed; use `--max-iterations` as a safety valve. `--strict` rejects these recursive term-generating cycles at analysis time. Head blank nodes are deterministically skolemized per rule and solution mapping, so the same firing reuses the same witness node instead of creating a fresh one each pass.
80
118
 
81
119
  ## Language surface
@@ -153,7 +191,6 @@ npm run w3c:rules:earl
153
191
  npm run w3c:rdf
154
192
  npm run w3c:rdf:json
155
193
  npm run w3c:rdf:earl
156
- npm run w3c:all
157
194
  ```
158
195
 
159
196
  `npm test` includes the W3C harnesses. When W3C URLs are reachable, progress is printed test by test. In offline environments, remote W3C checks are reported as unreachable unless `EYELENG_W3C_REQUIRED=1` is set. The `*:earl` scripts also print test progress, but write the EARL Turtle only to `reports/` instead of printing the report to the terminal.
@@ -186,15 +223,23 @@ The replay data includes message streams, envelopes, offsets, next-envelope link
186
223
 
187
224
  ## CLI
188
225
 
226
+ ```text
227
+ Usage: eyeleng [options] [file-or-url.n3|- ...]
228
+ ```
229
+
230
+ With no input arguments, Eyeleng prints help. Pass `-` to read from standard input; local files and HTTP(S) URLs can be combined as positional inputs.
231
+
189
232
  Common commands:
190
233
 
191
234
  ```sh
192
235
  ./eyeleng.js examples/family.srl
193
236
  ./eyeleng.js --all examples/family.srl
194
237
  ./eyeleng.js --check --deps examples/stratified-negation.srl
195
- ./eyeleng.js --json --trace --stats examples/if-then.srl
238
+ ./eyeleng.js --json --prove --stats examples/if-then.srl
196
239
  ./eyeleng.js --query-file examples/query-body.txt examples/query.srl
197
240
  ./eyeleng.js --syntax rdf examples/w3c-rule-set-snippet.ttl
241
+ cat examples/family.srl | ./eyeleng.js -
242
+ ./eyeleng.js https://example.org/rules.n3
198
243
  ```
199
244
 
200
245
  Important options:
@@ -202,7 +247,7 @@ Important options:
202
247
  ```text
203
248
  --all print the full closure, including input facts
204
249
  --json print JSON instead of compact triples/bindings
205
- --trace print derivation trace to stderr, or include it in JSON
250
+ --prove print proof explanations
206
251
  --stats print iteration and triple counts to stderr
207
252
  --check parse and analyze only; do not run rules
208
253
  --strict treat static warnings as errors, including recursive term generation
@@ -238,28 +283,30 @@ RULE { ?x a :Mortal } WHERE { ?x a :Man }
238
283
  console.log(formatTriples(result.inferred, result.prefixes));
239
284
  ```
240
285
 
241
- Query mode:
286
+ Hybrid and query mode:
242
287
 
243
288
  ```js
244
- const { runQuery, formatBindings } = require('./src/index.js');
289
+ const { run, runQuery, formatBindings } = require('./src/index.js');
245
290
 
291
+ // Normal run mode defaults to conservative auto-hybrid reasoning.
292
+ // Ordinary output rules still materialize forward, while selected
293
+ // function-like helper predicates can be proved backward on demand.
294
+ const resultWithAutoHybrid = run(source);
295
+
296
+ // Force pure forward closure if you want every derivable helper fact materialized.
297
+ const pureForward = run(source, { hybrid: false });
298
+
299
+ // For explicit queries, the whole raw body is the top-level goal.
300
+ // Each triple pattern in that body is a sub-goal for the planner.
246
301
  const result = runQuery(source, '?x :ancestorOf ?y');
247
302
  console.log(formatBindings(result.query.bindings, result.prefixes));
248
303
 
249
- // Query mode defaults to auto. Supported query/rule shapes are proved
250
- // backward with tabling. If full backward proving is not safe but the
251
- // ruleset contains function-like derived predicates, auto mode uses a
252
- // hybrid plan before falling back to plain forward closure.
253
- const justInTime = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
254
-
255
- // Run mode uses conservative auto-hybrid planning by default. It keeps
256
- // ordinary rules materialized, but can prove demanded function-like predicates
257
- // backward with tabling. Pass { hybrid: false } to force pure forward closure,
258
- // or { hybrid: true } to force aggressive hybrid orientation.
259
- const resultWithAutoHybrid = run(source);
304
+ // queryMode defaults to auto: try tabled backward proving for demanded
305
+ // predicates, then hybrid execution, then ordinary forward closure.
306
+ const justInTime = runQuery(source, ':alice :computedValue ?value');
260
307
 
261
- // The backward planner is demand-driven: irrelevant unsupported rules do not
262
- // prevent a query from using tabled backward proving.
308
+ // Force the tabled backward prover for supported query shapes.
309
+ const backwardOnly = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
263
310
  ```
264
311
 
265
312
  Imports:
@@ -326,6 +373,7 @@ Examples live in [examples/](./examples/):
326
373
  - `property-paths.srl` — path matching in bodies
327
374
  - `basic-ruleset.ttl` — RDF Rules syntax
328
375
  - `rdf-messages.srl` / `rdf-messages.trig` — RDF Message Log replay
376
+ - `fibonacci.srl` — fast-doubling Fibonacci using just-in-time backward helper predicates
329
377
  - `deep-taxonomy-*.srl` — generated benchmark programs
330
378
 
331
379
  ## Known boundaries
@@ -15,7 +15,7 @@
15
15
  const { parseRdfMessageLog, looksLikeRdfMessageLog } = require('./rdfMessages.js');
16
16
  const { evaluate } = require('./engine.js');
17
17
  const { analyze } = require('./analyze.js');
18
- const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
18
+ const { formatTriples, sortTriples, toJSON, formatTrace, formatProof, formatBindings } = require('./format.js');
19
19
  const { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');
20
20
  const { resultTriples } = require('./output.js');
21
21
 
@@ -138,6 +138,7 @@
138
138
  sortTriples,
139
139
  toJSON,
140
140
  formatTrace,
141
+ formatProof,
141
142
  resultTriples,
142
143
  };
143
144
 
@@ -441,7 +442,7 @@
441
442
  }
442
443
 
443
444
  parseTripleStatement(options = {}) {
444
- const subjectNode = this.parseGraphNode(options);
445
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
445
446
  const triples = [...subjectNode.triples];
446
447
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
447
448
  return triples;
@@ -455,7 +456,7 @@
455
456
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
456
457
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
457
458
  do {
458
- const objectNode = this.parseGraphNode(options);
459
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
459
460
  triples.push(...objectNode.triples);
460
461
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
461
462
  triples.push(baseTriple);
@@ -522,6 +523,7 @@
522
523
  currentReifier = this.parseOptionalReifier(options);
523
524
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
524
525
  } else if (this.matchValue('{|')) {
526
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
525
527
  const annotationSubject = currentReifier || this.freshGraphNode(options);
526
528
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
527
529
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -571,7 +573,7 @@
571
573
  }
572
574
 
573
575
  parseVerbTerm(options = {}) {
574
- const term = this.parseTerm(options);
576
+ const term = this.parseTerm({ ...options, position: 'predicate' });
575
577
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
576
578
  return term;
577
579
  }
@@ -694,7 +696,10 @@
694
696
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
695
697
  if (token.value === '<<') throw this.error('Use << s p o >> as a graph node reifier; use <<( s p o )>> for a triple term', token);
696
698
  if (token.type === 'word') {
697
- if (token.value === 'a') return iri(RDF_TYPE);
699
+ if (token.value === 'a') {
700
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
701
+ return iri(RDF_TYPE);
702
+ }
698
703
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
699
704
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
700
705
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -728,7 +733,13 @@
728
733
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
729
734
  }
730
735
  if (this.checkType('word') && /^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(this.peek().value)) {
731
- const tag = this.advance().value.slice(1).toLowerCase();
736
+ const tagToken = this.advance();
737
+ const rawTag = tagToken.value.slice(1);
738
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
739
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
740
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
741
+ }
742
+ const tag = rawTag.toLowerCase();
732
743
  const [lang, langDir = null] = tag.split('--');
733
744
  return literal(token.value, null, lang, langDir);
734
745
  }
@@ -2507,6 +2518,7 @@
2507
2518
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
2508
2519
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
2509
2520
  const implicitStatementNodes = new Set();
2521
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
2510
2522
 
2511
2523
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
2512
2524
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -2657,7 +2669,7 @@
2657
2669
  expect('>>');
2658
2670
  const node = reifier || freshBlank();
2659
2671
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
2660
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2672
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
2661
2673
  return node;
2662
2674
  }
2663
2675
 
@@ -2686,7 +2698,7 @@
2686
2698
  if (accept(']')) return node;
2687
2699
  parsePredicateObjectList(node, out, graph);
2688
2700
  expect(']');
2689
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2701
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
2690
2702
  return node;
2691
2703
  }
2692
2704
 
@@ -2764,9 +2776,8 @@
2764
2776
  if (options3.requireDot) expect('.'); else accept('.');
2765
2777
  return;
2766
2778
  }
2767
- if (peek()?.type === '<<' && peek(1)?.type === '(') throw new Error('Triple term cannot be used as a subject');
2768
- const subject = parseTerm(out, graph, { noLiteral: true, noA: true });
2769
- if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && subject.kind === 'blank' && implicitStatementNodes.has(subject.value)) {
2779
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
2780
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
2770
2781
  if (options3.requireDot) expect('.'); else accept('.');
2771
2782
  return;
2772
2783
  }
@@ -4253,6 +4264,7 @@
4253
4264
  rule: rule.name || `rule#${ruleIndex + 1}`,
4254
4265
  triple,
4255
4266
  binding,
4267
+ uses: proofUses(rule.body, binding),
4256
4268
  });
4257
4269
  }
4258
4270
  }
@@ -4263,6 +4275,17 @@
4263
4275
  return { applications, added };
4264
4276
  }
4265
4277
 
4278
+ function proofUses(body, binding) {
4279
+ return body
4280
+ .filter((clause) => clause.type === 'triple')
4281
+ .map((clause) => ({
4282
+ s: instantiateTerm(clause.triple.s, binding),
4283
+ p: instantiateTerm(clause.triple.p, binding),
4284
+ o: instantiateTerm(clause.triple.o, binding),
4285
+ }))
4286
+ .filter((triple) => ![triple.s, triple.p, triple.o].some((term) => term && term.type === 'var'));
4287
+ }
4288
+
4266
4289
  function prepareBodyContext(program, store, context) {
4267
4290
  if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
4268
4291
  return {
@@ -6053,6 +6076,35 @@
6053
6076
  return trace.map((entry) => `#${entry.iteration} ${entry.rule} => ${formatTriple(entry.triple, prefixes)}`).join('\n');
6054
6077
  }
6055
6078
 
6079
+ function formatProof(trace, prefixes = {}) {
6080
+ if (!trace.length) return '';
6081
+ const lines = ['@prefix pe: <https://eyereasoner.github.io/pe#> .', ''];
6082
+ for (const entry of trace) {
6083
+ const conclusion = formatTriple(entry.triple, prefixes);
6084
+ lines.push(`{ ${conclusion} } pe:why {`);
6085
+ lines.push(` { ${conclusion} }`);
6086
+ lines.push(` pe:by [ pe:rule ${quoteString(entry.rule)} ]${proofDetails(entry, prefixes)} .`);
6087
+ lines.push('}.', '');
6088
+ }
6089
+ return lines.join('\n').trimEnd();
6090
+ }
6091
+
6092
+ function proofDetails(entry, prefixes) {
6093
+ const details = [];
6094
+ const bindings = Object.entries(entry.binding || {}).sort(([a], [b]) => a.localeCompare(b));
6095
+ if (bindings.length > 0) {
6096
+ details.push(`\n pe:binding ${bindings.map(([name, value]) => `[ pe:var ${quoteString(name)}; pe:value ${formatTerm(value, prefixes)} ]`).join(', ')}`);
6097
+ }
6098
+ if (entry.uses && entry.uses.length > 0) {
6099
+ details.push(`\n pe:uses ${entry.uses.map((triple) => `{ ${formatTriple(triple, prefixes)} }`).join(', ')}`);
6100
+ }
6101
+ return details.length ? `;${details.join(';')}` : '';
6102
+ }
6103
+
6104
+ function quoteString(value) {
6105
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
6106
+ }
6107
+
6056
6108
  function formatBindings(bindings, prefixes = {}, select = null) {
6057
6109
  const columns = select && select.length > 0 ? select : inferColumns(bindings);
6058
6110
  return bindings
@@ -6084,7 +6136,7 @@
6084
6136
  prefixes: result.prefixes,
6085
6137
  diagnostics: result.diagnostics || [],
6086
6138
  triples: sortTriples(triples, result.prefixes).map(jsonSafeTriple),
6087
- trace: options.trace ? result.trace : undefined,
6139
+ proof: options.proof ? result.trace : undefined,
6088
6140
  };
6089
6141
  if (result.query) json.query = jsonSafeValue(result.query);
6090
6142
  if (result.analysis && options.analysis) json.analysis = result.analysis;
@@ -6113,7 +6165,7 @@
6113
6165
  return value;
6114
6166
  }
6115
6167
 
6116
- module.exports = { sortTriples, formatTriples, formatTrace, formatBindings, formatBinding, toJSON };
6168
+ module.exports = { sortTriples, formatTriples, formatTrace, formatProof, formatBindings, formatBinding, toJSON };
6117
6169
 
6118
6170
  },
6119
6171
  "src/query.js": function (require, module, exports) {
@@ -1,5 +1,7 @@
1
1
  # W3C SHACL 1.2 Rules draft examples
2
2
 
3
+ Eyeleng—where `leng` stands for **Logic Engine Next Generation**—is built for automatic hybrid reasoning, combining forward materialization with tabled backward proving.
4
+
3
5
  This directory contains runnable examples, including files mirrored from the SHACL 1.2 Rules draft.
4
6
  It also includes SRL adaptations of selected eyeling N3 examples.
5
7