eyeleng 1.1.0 → 1.1.2

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
@@ -8,7 +8,7 @@
8
8
  - **SRL** — the Shape Rules Language syntax used by the SHACL 1.2 Rules draft.
9
9
  - **RDF Rules** — a Turtle/RDF syntax for rule sets.
10
10
 
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.
11
+ Eyeleng is a compact reasoner over RDF-style triples. It still uses forward chaining for ordinary finite materialization, but the default execution mode now includes conservative auto-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 by 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
12
 
13
13
  Eyeleng implements the rules/reasoning surface. It is **not** a SHACL validation engine and does not emit SHACL validation reports.
14
14
 
@@ -76,6 +76,10 @@ then the body matches with `?parent = :alice` and `?child = :bob`, and the head
76
76
 
77
77
  Negation is handled by stratified evaluation: rules are grouped into dependency layers, and recursion through negation is rejected so the result stays deterministic.
78
78
 
79
+ 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.
80
+
81
+ 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.
82
+
79
83
  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
84
 
81
85
  ## Language surface
@@ -153,7 +157,6 @@ npm run w3c:rules:earl
153
157
  npm run w3c:rdf
154
158
  npm run w3c:rdf:json
155
159
  npm run w3c:rdf:earl
156
- npm run w3c:all
157
160
  ```
158
161
 
159
162
  `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.
@@ -210,7 +213,8 @@ Important options:
210
213
  --query TEXT run a raw SRL body pattern over the closure or backward planner
211
214
  --query-file FILE read a raw SRL body pattern from a file
212
215
  --query-mode MODE use auto, forward, or backward query planning (default auto)
213
- --hybrid orient function-like rules backward during forward execution and queries
216
+ --hybrid force aggressive hybrid orientation for function-like rules
217
+ --no-hybrid disable automatic hybrid forward/backward execution
214
218
  --max-iterations N stop after N fixpoint iterations within a recursive layer
215
219
  --no-imports parse IMPORTS/owl:imports but do not load imported rule sets
216
220
  --rdf-messages parse input as an RDF Message Log
@@ -237,27 +241,30 @@ RULE { ?x a :Mortal } WHERE { ?x a :Man }
237
241
  console.log(formatTriples(result.inferred, result.prefixes));
238
242
  ```
239
243
 
240
- Query mode:
244
+ Hybrid and query mode:
241
245
 
242
246
  ```js
243
- const { runQuery, formatBindings } = require('./src/index.js');
247
+ const { run, runQuery, formatBindings } = require('./src/index.js');
248
+
249
+ // Normal run mode defaults to conservative auto-hybrid reasoning.
250
+ // Ordinary output rules still materialize forward, while selected
251
+ // function-like helper predicates can be proved backward on demand.
252
+ const resultWithAutoHybrid = run(source);
244
253
 
254
+ // Force pure forward closure if you want every derivable helper fact materialized.
255
+ const pureForward = run(source, { hybrid: false });
256
+
257
+ // For explicit queries, the whole raw body is the top-level goal.
258
+ // Each triple pattern in that body is a sub-goal for the planner.
245
259
  const result = runQuery(source, '?x :ancestorOf ?y');
246
260
  console.log(formatBindings(result.query.bindings, result.prefixes));
247
261
 
248
- // Query mode defaults to auto. Supported query/rule shapes are proved
249
- // backward with tabling. If full backward proving is not safe but the
250
- // ruleset contains function-like derived predicates, auto mode uses a
251
- // hybrid plan before falling back to plain forward closure.
252
- const justInTime = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
253
-
254
- // Explicit hybrid mode keeps forward materialization for ordinary rules, but
255
- // orients function-like derived predicates backward and proves them only when
256
- // a forward rule body or query asks for them.
257
- const hybrid = run(source, { hybrid: true });
262
+ // queryMode defaults to auto: try tabled backward proving for demanded
263
+ // predicates, then hybrid execution, then ordinary forward closure.
264
+ const justInTime = runQuery(source, ':alice :computedValue ?value');
258
265
 
259
- // The backward planner is demand-driven: irrelevant unsupported rules do not
260
- // prevent a query from using tabled backward proving.
266
+ // Force the tabled backward prover for supported query shapes.
267
+ const backwardOnly = runQuery(source, ':alice :computedValue ?value', { queryMode: 'backward' });
261
268
  ```
262
269
 
263
270
  Imports:
@@ -324,6 +331,7 @@ Examples live in [examples/](./examples/):
324
331
  - `property-paths.srl` — path matching in bodies
325
332
  - `basic-ruleset.ttl` — RDF Rules syntax
326
333
  - `rdf-messages.srl` / `rdf-messages.trig` — RDF Message Log replay
334
+ - `fibonacci.srl` — fast-doubling Fibonacci using just-in-time backward helper predicates
327
335
  - `deep-taxonomy-*.srl` — generated benchmark programs
328
336
 
329
337
  ## Known boundaries
@@ -441,7 +441,7 @@
441
441
  }
442
442
 
443
443
  parseTripleStatement(options = {}) {
444
- const subjectNode = this.parseGraphNode(options);
444
+ const subjectNode = this.parseGraphNode({ ...options, position: 'subject' });
445
445
  const triples = [...subjectNode.triples];
446
446
  triples.push(...this.parsePropertyListForSubject(subjectNode.term, options));
447
447
  return triples;
@@ -455,7 +455,7 @@
455
455
  if (terminators.some((value) => this.checkValue(value)) || this.checkValue('.')) break;
456
456
  const predicate = options.allowPath ? this.parseVerbPathOrSimple(options) : this.parseVerbTerm(options);
457
457
  do {
458
- const objectNode = this.parseGraphNode(options);
458
+ const objectNode = this.parseGraphNode({ ...options, position: 'object' });
459
459
  triples.push(...objectNode.triples);
460
460
  const baseTriple = { s: subject, p: predicate, o: objectNode.term };
461
461
  triples.push(baseTriple);
@@ -522,6 +522,7 @@
522
522
  currentReifier = this.parseOptionalReifier(options);
523
523
  triples.push({ s: currentReifier, p: iri(RDF_REIFIES), o: reified });
524
524
  } else if (this.matchValue('{|')) {
525
+ if (this.checkValue('|}')) throw this.error('Annotation blocks may not be empty');
525
526
  const annotationSubject = currentReifier || this.freshGraphNode(options);
526
527
  triples.push({ s: annotationSubject, p: iri(RDF_REIFIES), o: reified });
527
528
  triples.push(...this.parsePropertyListForSubject(annotationSubject, options, ['|}']));
@@ -571,7 +572,7 @@
571
572
  }
572
573
 
573
574
  parseVerbTerm(options = {}) {
574
- const term = this.parseTerm(options);
575
+ const term = this.parseTerm({ ...options, position: 'predicate' });
575
576
  if (term.type !== 'iri' && term.type !== 'var') throw this.error('Expected IRI or variable as predicate');
576
577
  return term;
577
578
  }
@@ -694,7 +695,10 @@
694
695
  if (token.value === '<<(') return this.parseTripleTermAfterOpen(options);
695
696
  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
697
  if (token.type === 'word') {
697
- if (token.value === 'a') return iri(RDF_TYPE);
698
+ if (token.value === 'a') {
699
+ if (options.position !== 'predicate') throw this.error('a is only allowed as a predicate', token);
700
+ return iri(RDF_TYPE);
701
+ }
698
702
  if (token.value === 'true') return literal(true, XSD_BOOLEAN);
699
703
  if (token.value === 'false') return literal(false, XSD_BOOLEAN);
700
704
  if (token.value.startsWith('_:')) return blankNode(token.value.slice(2));
@@ -728,7 +732,13 @@
728
732
  return literal(coerceLexicalLiteral(token.value, datatype), datatype, null);
729
733
  }
730
734
  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();
735
+ const tagToken = this.advance();
736
+ const rawTag = tagToken.value.slice(1);
737
+ const direction = rawTag.includes('--') ? rawTag.slice(rawTag.lastIndexOf('--') + 2) : null;
738
+ if (direction && direction !== 'ltr' && direction !== 'rtl') {
739
+ throw this.error(`Invalid base direction --${direction}; expected --ltr or --rtl`, tagToken);
740
+ }
741
+ const tag = rawTag.toLowerCase();
732
742
  const [lang, langDir = null] = tag.split('--');
733
743
  return literal(token.value, null, lang, langDir);
734
744
  }
@@ -2507,6 +2517,7 @@
2507
2517
  const syntaxProfile = String(options.profile || options.profileId || '').toLowerCase();
2508
2518
  const rdf12Surface = syntaxProfile === 'turtle' || syntaxProfile === 'trig';
2509
2519
  const implicitStatementNodes = new Set();
2520
+ function implicitStatementNodeKey(term) { return `${term.kind}:${term.value}`; }
2510
2521
 
2511
2522
  function freshBlank() { bnodeCounter += 1; return blank(`b${bnodeCounter}`); }
2512
2523
  function peek(offset = 0) { return tokens[i + offset]; }
@@ -2657,7 +2668,7 @@
2657
2668
  expect('>>');
2658
2669
  const node = reifier || freshBlank();
2659
2670
  out.push(triple(node, iri(RDF_REIFIES), tripleTerm(s, p, o), graph));
2660
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2671
+ implicitStatementNodes.add(implicitStatementNodeKey(node));
2661
2672
  return node;
2662
2673
  }
2663
2674
 
@@ -2686,7 +2697,7 @@
2686
2697
  if (accept(']')) return node;
2687
2698
  parsePredicateObjectList(node, out, graph);
2688
2699
  expect(']');
2689
- if (node.kind === 'blank') implicitStatementNodes.add(node.value);
2700
+ if (node.kind === 'blank') implicitStatementNodes.add(implicitStatementNodeKey(node));
2690
2701
  return node;
2691
2702
  }
2692
2703
 
@@ -2764,9 +2775,8 @@
2764
2775
  if (options3.requireDot) expect('.'); else accept('.');
2765
2776
  return;
2766
2777
  }
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)) {
2778
+ const subject = parseTerm(out, graph, { noLiteral: !rdf12Surface, noA: true });
2779
+ if ((peek()?.type === '.' || peek()?.type === '}' || peek()?.type === undefined) && implicitStatementNodes.has(implicitStatementNodeKey(subject))) {
2770
2780
  if (options3.requireDot) expect('.'); else accept('.');
2771
2781
  return;
2772
2782
  }
@@ -4088,7 +4098,8 @@
4088
4098
  const relaxedRecursiveRunOnce = options.relaxedRecursion === false
4089
4099
  ? new Set()
4090
4100
  : recursiveTermGenerationRuleIndexes(analysis);
4091
- const hybridBackwardPredicates = options.hybrid || options.backwardBodyCalls
4101
+ const useHybrid = options.hybrid !== false && !options.shacl12Conformance;
4102
+ const hybridBackwardPredicates = useHybrid || options.backwardBodyCalls
4092
4103
  ? preferredBackwardPredicates(program, options)
4093
4104
  : new Set();
4094
4105
  const hybridBackwardRules = new Set();
@@ -5624,10 +5635,7 @@
5624
5635
  const entry = this.memo.get(key);
5625
5636
  if (entry && entry.complete) {
5626
5637
  this.stats.memoHits += 1;
5627
- for (const answer of entry.answers) {
5628
- const next = unifyTriples(pattern, answer, binding);
5629
- if (next) yield next;
5630
- }
5638
+ yield* this.replayAnswers(pattern, binding, entry.answers);
5631
5639
  return;
5632
5640
  }
5633
5641
  if (this.active.has(key)) return;
@@ -5641,7 +5649,6 @@
5641
5649
  if (!next) continue;
5642
5650
  rememberAnswer(answers, answerKeys, pattern, next);
5643
5651
  this.stats.facts += 1;
5644
- yield next;
5645
5652
  }
5646
5653
 
5647
5654
  for (const item of this.ruleCandidates(resolvedPattern)) {
@@ -5653,7 +5660,6 @@
5653
5660
  for (const solved of this.solveBody(freshBody, next, depth + 1, 0)) {
5654
5661
  rememberAnswer(answers, answerKeys, pattern, solved);
5655
5662
  this.stats.rules += 1;
5656
- yield solved;
5657
5663
  }
5658
5664
  }
5659
5665
  } finally {
@@ -5662,6 +5668,14 @@
5662
5668
 
5663
5669
  this.memo.set(key, { complete: true, answers });
5664
5670
  this.stats.memoStores += 1;
5671
+ yield* this.replayAnswers(pattern, binding, answers);
5672
+ }
5673
+
5674
+ *replayAnswers(pattern, binding, answers) {
5675
+ for (const answer of answers) {
5676
+ const next = unifyTriples(pattern, answer, binding);
5677
+ if (next) yield next;
5678
+ }
5665
5679
  }
5666
5680
 
5667
5681
  factCandidates(pattern, binding) {
@@ -5941,15 +5955,57 @@
5941
5955
  if (explicit) return supportedBackwardPredicates(program, { ...options, hybridPredicates: explicit });
5942
5956
  const supported = supportedBackwardPredicates(program, options);
5943
5957
  const preferred = new Set();
5958
+ const force = options.hybrid === true || options.hybridMode === 'force';
5959
+ const demanded = force ? null : demandedBodyPredicates(program);
5944
5960
  for (const rule of program.rules || []) {
5945
5961
  if (!ruleIsFunctionLike(rule)) continue;
5962
+ if (!force && ruleCreatesHeadTerms(rule)) continue;
5946
5963
  for (const head of rule.head || []) {
5947
- if (head && head.p && head.p.type === 'iri' && supported.has(head.p.value)) preferred.add(head.p.value);
5964
+ if (!head || !head.p || head.p.type !== 'iri' || !supported.has(head.p.value)) continue;
5965
+ if (force || demanded.has(head.p.value)) preferred.add(head.p.value);
5948
5966
  }
5949
5967
  }
5950
5968
  return preferred;
5951
5969
  }
5952
5970
 
5971
+ function demandedBodyPredicates(program) {
5972
+ const out = new Set();
5973
+ for (const rule of program.rules || []) {
5974
+ for (const predicate of bodyPredicateDemands(rule.body || [])) if (predicate) out.add(predicate);
5975
+ }
5976
+ return out;
5977
+ }
5978
+
5979
+
5980
+ function ruleCreatesHeadTerms(rule) {
5981
+ const headVars = new Set();
5982
+ for (const triple of rule.head || []) {
5983
+ for (const term of [triple.s, triple.p, triple.o]) {
5984
+ if (!term) continue;
5985
+ if (term.type === 'blank') return true;
5986
+ if (term.type === 'var') headVars.add(term.value);
5987
+ }
5988
+ }
5989
+ if (headVars.size === 0) return false;
5990
+ for (const clause of rule.body || []) {
5991
+ if ((clause.type === 'set' || clause.type === 'bind') && headVars.has(clause.variable) && expressionCreatesTerm(clause.expr)) return true;
5992
+ }
5993
+ return false;
5994
+ }
5995
+
5996
+ function expressionCreatesTerm(expr) {
5997
+ if (!expr) return false;
5998
+ if (expr.type === 'call') {
5999
+ const name = String(expr.name || '').toUpperCase();
6000
+ if (name === 'BNODE' || name === 'IRI' || name === 'URI' || name === 'TRIPLE' || name === 'UUID' || name === 'STRUUID') return true;
6001
+ return (expr.args || []).some(expressionCreatesTerm);
6002
+ }
6003
+ if (expr.type === 'binary') return expressionCreatesTerm(expr.left) || expressionCreatesTerm(expr.right);
6004
+ if (expr.type === 'unary') return expressionCreatesTerm(expr.expr);
6005
+ if (expr.type === 'in') return expressionCreatesTerm(expr.left) || (expr.values || []).some(expressionCreatesTerm);
6006
+ return false;
6007
+ }
6008
+
5953
6009
  function ruleIsFunctionLike(rule) {
5954
6010
  return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
5955
6011
  }
@@ -6087,7 +6143,7 @@
6087
6143
  prefixes: result.prefixes,
6088
6144
  select,
6089
6145
  bindings: projectBindings(bindings, select),
6090
- mode: options.queryMode === 'hybrid' || options.hybrid ? 'hybrid' : 'forward',
6146
+ mode: result.hybridStats ? 'hybrid' : 'forward',
6091
6147
  };
6092
6148
  }
6093
6149
 
@@ -6153,13 +6209,16 @@
6153
6209
  }
6154
6210
 
6155
6211
  function queryRunOptions(program, querySpec, options = {}) {
6156
- if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: true };
6212
+ const mode = options.queryMode || 'auto';
6213
+ if (mode === 'forward') return { ...options, hybrid: false };
6214
+ if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: options.hybrid ?? 'auto' };
6157
6215
  return options;
6158
6216
  }
6159
6217
 
6160
6218
  function shouldUseHybridForQuery(program, querySpec, options = {}) {
6161
6219
  const mode = options.queryMode || 'auto';
6162
- if (options.hybrid || mode === 'hybrid') return true;
6220
+ if (options.hybrid === false) return false;
6221
+ if (options.hybrid === true) return true;
6163
6222
  if (mode !== 'auto') return false;
6164
6223
  if (!querySpec) return false;
6165
6224
  return preferredBackwardPredicates(program, options).size > 0;