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 +25 -17
- package/dist/browser/eyeleng.browser.js +80 -21
- package/examples/fibonacci.srl +89 -30009
- package/examples/output/bayes-diagnosis.trig +0 -4
- package/examples/output/bmi.trig +0 -2
- package/examples/output/dijkstra.trig +0 -8
- package/examples/output/fibonacci.trig +2 -19996
- package/examples/output/hanoi.trig +0 -10
- package/examples/output/spec-2-5-assignment-with-negation.trig +0 -1
- package/examples/output/sudoku.trig +0 -1
- package/examples/output/turing.trig +0 -5
- package/eyeleng.js +83 -23
- package/package.json +1 -1
- package/reports/w3c-shacl12-rules-earl.ttl +89 -89
- package/src/backward.js +52 -7
- package/src/cli.js +3 -2
- package/src/engine.js +2 -1
- package/src/parser.js +15 -5
- package/src/query.js +6 -3
- package/src/rdfSyntax.js +5 -5
- package/test/api.test.js +89 -0
- package/test/cli.test.js +2 -0
- package/test/perf-baseline.json +7 -7
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
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
|
-
//
|
|
260
|
-
|
|
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')
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2768
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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;
|