eyeleng 1.0.13 → 1.1.1
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 +24 -5
- package/dist/browser/eyeleng.browser.js +686 -14
- 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 +735 -24
- package/package.json +1 -1
- package/reports/w3c-shacl12-rules-earl.ttl +89 -89
- package/src/api.js +4 -1
- package/src/backward.js +535 -0
- package/src/cli.js +49 -10
- package/src/engine.js +69 -5
- package/src/query.js +72 -7
- package/src/store.js +2 -0
- package/test/api.test.js +181 -1
- package/test/cli.test.js +13 -2
- package/test/perf-baseline.json +7 -7
package/src/engine.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { TripleStore, bindingKey } = require('./store.js');
|
|
3
|
+
const { TripleStore, bindingKey, instantiateTerm } = require('./store.js');
|
|
4
4
|
const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
|
|
5
5
|
const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
|
|
6
6
|
const { analyze } = require('./analyze.js');
|
|
7
|
+
const { BackwardProver, preferredBackwardPredicates, ruleIsBackwardOriented } = require('./backward.js');
|
|
7
8
|
|
|
8
9
|
function evaluate(program, options = {}) {
|
|
9
10
|
const maxIterations = options.maxIterations ?? 10000;
|
|
@@ -19,6 +20,7 @@ function evaluate(program, options = {}) {
|
|
|
19
20
|
applications: 0,
|
|
20
21
|
added: 0,
|
|
21
22
|
runOnce: !!rule.runOnce,
|
|
23
|
+
backward: false,
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
26
|
const analysis = options.analysis || analyze(program, options);
|
|
@@ -35,6 +37,18 @@ function evaluate(program, options = {}) {
|
|
|
35
37
|
const relaxedRecursiveRunOnce = options.relaxedRecursion === false
|
|
36
38
|
? new Set()
|
|
37
39
|
: recursiveTermGenerationRuleIndexes(analysis);
|
|
40
|
+
const useHybrid = options.hybrid !== false && !options.shacl12Conformance;
|
|
41
|
+
const hybridBackwardPredicates = useHybrid || options.backwardBodyCalls
|
|
42
|
+
? preferredBackwardPredicates(program, options)
|
|
43
|
+
: new Set();
|
|
44
|
+
const hybridBackwardRules = new Set();
|
|
45
|
+
if (hybridBackwardPredicates.size > 0) {
|
|
46
|
+
for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {
|
|
47
|
+
if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;
|
|
51
|
+
for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;
|
|
38
52
|
const baseContext = {
|
|
39
53
|
...evalOptions,
|
|
40
54
|
maxIterations,
|
|
@@ -46,12 +60,16 @@ function evaluate(program, options = {}) {
|
|
|
46
60
|
iteration: 0,
|
|
47
61
|
startingIterations: 0,
|
|
48
62
|
recursiveLayer: false,
|
|
63
|
+
hybridBackwardPredicates,
|
|
64
|
+
hybridBackwardRules,
|
|
65
|
+
hybridStats,
|
|
49
66
|
};
|
|
50
67
|
|
|
51
68
|
for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
|
|
52
69
|
const layer = layerIndexes[layerIndex];
|
|
53
|
-
const
|
|
54
|
-
const
|
|
70
|
+
const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;
|
|
71
|
+
const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
|
|
72
|
+
const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
|
|
55
73
|
|
|
56
74
|
if (runOnce.length > 0) {
|
|
57
75
|
iterations += 1;
|
|
@@ -84,6 +102,7 @@ function evaluate(program, options = {}) {
|
|
|
84
102
|
ruleApplications,
|
|
85
103
|
perRule,
|
|
86
104
|
trace,
|
|
105
|
+
hybridStats,
|
|
87
106
|
};
|
|
88
107
|
}
|
|
89
108
|
|
|
@@ -153,9 +172,10 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
153
172
|
const seenBindings = dedupeBindings ? new Set() : null;
|
|
154
173
|
const headBlankLabels = collectHeadBlankLabels(rule.head);
|
|
155
174
|
|
|
156
|
-
const
|
|
175
|
+
const bodyContext = prepareBodyContext(program, store, context);
|
|
176
|
+
const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)
|
|
157
177
|
? store.match(rule.body[0].triple, {})
|
|
158
|
-
: evaluateBodyStream(rule.body, store, {},
|
|
178
|
+
: evaluateBodyStream(rule.body, store, {}, bodyContext);
|
|
159
179
|
|
|
160
180
|
for (const binding of bodyBindings) {
|
|
161
181
|
if (seenBindings) {
|
|
@@ -188,9 +208,36 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
188
208
|
}
|
|
189
209
|
}
|
|
190
210
|
|
|
211
|
+
if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);
|
|
191
212
|
return { applications, added };
|
|
192
213
|
}
|
|
193
214
|
|
|
215
|
+
function prepareBodyContext(program, store, context) {
|
|
216
|
+
if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
|
|
217
|
+
return {
|
|
218
|
+
...context,
|
|
219
|
+
backwardProver: new BackwardProver(program, {
|
|
220
|
+
...context,
|
|
221
|
+
store,
|
|
222
|
+
allowedPredicates: context.hybridBackwardPredicates,
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function emptyBackwardStats() {
|
|
228
|
+
return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function mergeBackwardStats(total, item) {
|
|
232
|
+
if (!total || !item) return;
|
|
233
|
+
total.goals += item.goals || 0;
|
|
234
|
+
total.facts += item.facts || 0;
|
|
235
|
+
total.rules += item.rules || 0;
|
|
236
|
+
total.memoHits += item.memoHits || 0;
|
|
237
|
+
total.memoStores += item.memoStores || 0;
|
|
238
|
+
total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);
|
|
239
|
+
}
|
|
240
|
+
|
|
194
241
|
function recursiveTermGenerationRuleIndexes(analysis) {
|
|
195
242
|
const out = new Set();
|
|
196
243
|
if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
|
|
@@ -299,9 +346,20 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
|
|
|
299
346
|
|
|
300
347
|
const clause = clauses[index];
|
|
301
348
|
if (clause.type === 'triple') {
|
|
349
|
+
const seen = new Set();
|
|
302
350
|
for (const matched of store.match(clause.triple, initialBinding)) {
|
|
351
|
+
const key = bindingKey(matched);
|
|
352
|
+
seen.add(key);
|
|
303
353
|
yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
|
|
304
354
|
}
|
|
355
|
+
if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {
|
|
356
|
+
for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {
|
|
357
|
+
const key = bindingKey(matched);
|
|
358
|
+
if (seen.has(key)) continue;
|
|
359
|
+
seen.add(key);
|
|
360
|
+
yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
305
363
|
return;
|
|
306
364
|
}
|
|
307
365
|
|
|
@@ -347,6 +405,12 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
|
|
|
347
405
|
throw new Error(`Unsupported body clause ${clause.type}`);
|
|
348
406
|
}
|
|
349
407
|
|
|
408
|
+
function shouldUseBackwardForTriple(pattern, binding, options = {}) {
|
|
409
|
+
if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;
|
|
410
|
+
const predicate = instantiateTerm(pattern.p, binding);
|
|
411
|
+
return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));
|
|
412
|
+
}
|
|
413
|
+
|
|
350
414
|
function bodyHasAny(clauses, store, initialBinding, options) {
|
|
351
415
|
for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;
|
|
352
416
|
return false;
|
package/src/query.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { parseQuery } = require('./parser.js');
|
|
4
4
|
const { TripleStore, bindingKey } = require('./store.js');
|
|
5
5
|
const { evaluateBody } = require('./engine.js');
|
|
6
|
+
const { backwardQuery, planBackwardQuery, preferredBackwardPredicates } = require('./backward.js');
|
|
6
7
|
|
|
7
8
|
function queryResult(result, querySpec, options = {}) {
|
|
8
9
|
const store = new TripleStore(result.closure || []);
|
|
@@ -13,28 +14,92 @@ function queryResult(result, querySpec, options = {}) {
|
|
|
13
14
|
prefixes: result.prefixes,
|
|
14
15
|
select,
|
|
15
16
|
bindings: projectBindings(bindings, select),
|
|
17
|
+
mode: result.hybridStats ? 'hybrid' : 'forward',
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
function queryProgram(program, querySpec, options = {}) {
|
|
22
|
+
const mode = options.queryMode || 'auto';
|
|
23
|
+
if (mode !== 'forward' && mode !== 'hybrid') {
|
|
24
|
+
const planned = planBackwardQuery(program, querySpec, options);
|
|
25
|
+
if (planned.ok) {
|
|
26
|
+
const result = backwardQuery(program, querySpec, options);
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
const select = normalizeSelect(querySpec.select, result.bindings);
|
|
29
|
+
return {
|
|
30
|
+
baseIRI: program.baseIRI,
|
|
31
|
+
prefixes: program.prefixes,
|
|
32
|
+
select,
|
|
33
|
+
bindings: projectBindings(result.bindings, select),
|
|
34
|
+
mode: 'backward',
|
|
35
|
+
stats: result.stats,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (mode === 'backward') throw new Error(result.reason || 'Backward query failed');
|
|
39
|
+
} else if (mode === 'backward') {
|
|
40
|
+
throw new Error(`Backward query is not supported for this ruleset: ${planned.reason}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
function runQuery(source, querySource = null, options = {}) {
|
|
20
47
|
const { run, compile } = require('./api.js');
|
|
21
|
-
const { program, diagnostics } = compile(source, options);
|
|
22
|
-
const result = run(program, options);
|
|
23
|
-
result.diagnostics = diagnostics;
|
|
48
|
+
const { program, diagnostics, analysis } = compile(source, options);
|
|
24
49
|
|
|
25
50
|
let querySpec;
|
|
26
51
|
if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });
|
|
27
52
|
else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');
|
|
28
53
|
|
|
29
|
-
const
|
|
30
|
-
|
|
54
|
+
const direct = queryProgram(program, querySpec, options);
|
|
55
|
+
if (direct) {
|
|
56
|
+
return {
|
|
57
|
+
baseIRI: program.baseIRI,
|
|
58
|
+
version: program.version || null,
|
|
59
|
+
imports: program.imports || [],
|
|
60
|
+
prefixes: program.prefixes,
|
|
61
|
+
input: program.data.slice(),
|
|
62
|
+
inferred: [],
|
|
63
|
+
closure: program.data.slice(),
|
|
64
|
+
iterations: 0,
|
|
65
|
+
layers: [],
|
|
66
|
+
ruleApplications: 0,
|
|
67
|
+
perRule: [],
|
|
68
|
+
trace: [],
|
|
69
|
+
diagnostics,
|
|
70
|
+
analysis,
|
|
71
|
+
query: direct,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const runOptions = queryRunOptions(program, querySpec, options);
|
|
76
|
+
const result = run(program, runOptions);
|
|
77
|
+
result.diagnostics = diagnostics;
|
|
78
|
+
result.query = queryResult(result, querySpec, runOptions);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function queryRunOptions(program, querySpec, options = {}) {
|
|
83
|
+
const mode = options.queryMode || 'auto';
|
|
84
|
+
if (mode === 'forward') return { ...options, hybrid: false };
|
|
85
|
+
if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: options.hybrid ?? 'auto' };
|
|
86
|
+
return options;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shouldUseHybridForQuery(program, querySpec, options = {}) {
|
|
90
|
+
const mode = options.queryMode || 'auto';
|
|
91
|
+
if (options.hybrid === false) return false;
|
|
92
|
+
if (options.hybrid === true) return true;
|
|
93
|
+
if (mode !== 'auto') return false;
|
|
94
|
+
if (!querySpec) return false;
|
|
95
|
+
return preferredBackwardPredicates(program, options).size > 0;
|
|
31
96
|
}
|
|
32
97
|
|
|
33
98
|
function normalizeSelect(select, bindings) {
|
|
34
99
|
if (select && select.length > 0) return select.slice();
|
|
35
100
|
const vars = new Set();
|
|
36
101
|
for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);
|
|
37
|
-
return Array.from(vars).sort();
|
|
102
|
+
return Array.from(vars).sort().filter((name) => !name.includes('__b'));
|
|
38
103
|
}
|
|
39
104
|
|
|
40
105
|
function projectBindings(bindings, select) {
|
|
@@ -52,4 +117,4 @@ function projectBindings(bindings, select) {
|
|
|
52
117
|
return out;
|
|
53
118
|
}
|
|
54
119
|
|
|
55
|
-
module.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };
|
|
120
|
+
module.exports = { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery, parseQuery, normalizeSelect, projectBindings };
|
package/src/store.js
CHANGED
|
@@ -8,6 +8,7 @@ class TripleStore {
|
|
|
8
8
|
this.byPredicate = new Map();
|
|
9
9
|
this.byPredicateSubject = new Map();
|
|
10
10
|
this.byPredicateObject = new Map();
|
|
11
|
+
this.version = 0;
|
|
11
12
|
for (const triple of triples) this.add(triple);
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -22,6 +23,7 @@ class TripleStore {
|
|
|
22
23
|
addIndex(this.byPredicate, predicate, key, normalized);
|
|
23
24
|
addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);
|
|
24
25
|
addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);
|
|
26
|
+
this.version += 1;
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
27
29
|
|
package/test/api.test.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { test, main } = require('./harness.js').createHarness('API');
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
|
-
const { parse, compile, run, runToString } = require('../src/index.js');
|
|
5
|
+
const { parse, compile, run, runToString, runQuery } = require('../src/index.js');
|
|
6
6
|
const { tripleKey } = require('../src/term.js');
|
|
7
7
|
|
|
8
8
|
test('parse reads prefixes, data, and rules', () => {
|
|
@@ -159,6 +159,168 @@ RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
|
|
|
159
159
|
assert.match(output, /\?d = :carol/);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
test('backward query mode proves recursive derived predicates with tabling', () => {
|
|
165
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
166
|
+
const result = runQuery(`
|
|
167
|
+
PREFIX : <http://example/>
|
|
168
|
+
DATA { :alice :parentOf :bob . :bob :parentOf :carol . }
|
|
169
|
+
RULE { ?x :ancestorOf ?y } WHERE { ?x :parentOf ?y }
|
|
170
|
+
RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
|
|
171
|
+
`, ':alice :ancestorOf ?d', { queryMode: 'backward' });
|
|
172
|
+
assert.equal(result.iterations, 0);
|
|
173
|
+
assert.equal(result.query.mode, 'backward');
|
|
174
|
+
assert.ok(result.query.stats.memoStores > 0);
|
|
175
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
176
|
+
assert.match(output, /\?d = :bob/);
|
|
177
|
+
assert.match(output, /\?d = :carol/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('backward query mode computes function-like rules just in time', () => {
|
|
181
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
182
|
+
const result = runQuery(`
|
|
183
|
+
PREFIX : <http://example/>
|
|
184
|
+
DATA { :alice :score 41 . :bob :score 2 . }
|
|
185
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
186
|
+
`, '?who :nextScore ?score', { queryMode: 'backward' });
|
|
187
|
+
assert.equal(result.iterations, 0);
|
|
188
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
189
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
190
|
+
assert.match(output, /\?score = 3; \?who = :bob/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('auto query mode uses backward planning for supported rules', () => {
|
|
194
|
+
const { runQuery } = require('../src/index.js');
|
|
195
|
+
const result = runQuery(`
|
|
196
|
+
PREFIX : <http://example/>
|
|
197
|
+
DATA { :a :p :b . }
|
|
198
|
+
RULE { ?x :q ?y } WHERE { ?x :p ?y }
|
|
199
|
+
`, ':a :q ?y');
|
|
200
|
+
assert.equal(result.query.mode, 'backward');
|
|
201
|
+
assert.equal(result.iterations, 0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
test('hybrid execution proves function-like body predicates backward without materializing them', () => {
|
|
207
|
+
const result = run(`
|
|
208
|
+
PREFIX : <http://example/>
|
|
209
|
+
DATA { :alice :score 41 . :bob :score 2 . }
|
|
210
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
211
|
+
RULE { ?x :ready true } WHERE { ?x :nextScore 42 }
|
|
212
|
+
`, { hybrid: true });
|
|
213
|
+
const keys = result.closure.map(tripleKey).join('\n');
|
|
214
|
+
assert.match(keys, /ready/);
|
|
215
|
+
assert.doesNotMatch(keys, /nextScore/);
|
|
216
|
+
assert.equal(result.perRule[0].applications, 0);
|
|
217
|
+
assert.equal(result.perRule[0].backward, true);
|
|
218
|
+
assert.ok(result.hybridStats.goals > 0);
|
|
219
|
+
assert.ok(result.hybridStats.rules > 0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
test('hybrid backward tabling completes sibling recursive goals before replaying answers', () => {
|
|
225
|
+
const result = run(`
|
|
226
|
+
PREFIX : <http://example/fib/>
|
|
227
|
+
DATA {
|
|
228
|
+
:n0 :index 0 ; :fibPairF 0 ; :fibPairG 1 .
|
|
229
|
+
:n1 :index 1 .
|
|
230
|
+
:n2 :index 2 .
|
|
231
|
+
:n5 :index 5 .
|
|
232
|
+
:n10 a :FibCase ; :index 10 .
|
|
233
|
+
}
|
|
234
|
+
RULE { ?node :fibPairF ?c ; :fibPairG ?d }
|
|
235
|
+
WHERE {
|
|
236
|
+
?node :index ?n .
|
|
237
|
+
FILTER(?n > 0)
|
|
238
|
+
SET(?half := FLOOR(?n / 2))
|
|
239
|
+
SET(?halfNode := IRI(CONCAT("http://example/fib/n", STR(?half))))
|
|
240
|
+
?halfNode :fibPairF ?a ; :fibPairG ?b .
|
|
241
|
+
SET(?twob := ?b * 2)
|
|
242
|
+
SET(?twobminusa := ?twob - ?a)
|
|
243
|
+
SET(?c := ?a * ?twobminusa)
|
|
244
|
+
SET(?aa := ?a * ?a)
|
|
245
|
+
SET(?bb := ?b * ?b)
|
|
246
|
+
SET(?d := ?aa + ?bb)
|
|
247
|
+
SET(?parity := ?n - (?half * 2))
|
|
248
|
+
FILTER(?parity = 0)
|
|
249
|
+
}
|
|
250
|
+
RULE { ?node :fibPairF ?d ; :fibPairG ?next }
|
|
251
|
+
WHERE {
|
|
252
|
+
?node :index ?n .
|
|
253
|
+
FILTER(?n > 0)
|
|
254
|
+
SET(?half := FLOOR(?n / 2))
|
|
255
|
+
SET(?halfNode := IRI(CONCAT("http://example/fib/n", STR(?half))))
|
|
256
|
+
?halfNode :fibPairF ?a ; :fibPairG ?b .
|
|
257
|
+
SET(?twob := ?b * 2)
|
|
258
|
+
SET(?twobminusa := ?twob - ?a)
|
|
259
|
+
SET(?c := ?a * ?twobminusa)
|
|
260
|
+
SET(?aa := ?a * ?a)
|
|
261
|
+
SET(?bb := ?b * ?b)
|
|
262
|
+
SET(?d := ?aa + ?bb)
|
|
263
|
+
SET(?parity := ?n - (?half * 2))
|
|
264
|
+
FILTER(?parity = 1)
|
|
265
|
+
SET(?next := ?c + ?d)
|
|
266
|
+
}
|
|
267
|
+
RULE { ?node :fib ?value } WHERE { ?node a :FibCase ; :fibPairF ?value }
|
|
268
|
+
`, { hybrid: true });
|
|
269
|
+
const keys = result.closure.map(tripleKey).join('\n');
|
|
270
|
+
assert.match(keys, /I:http:\/\/example\/fib\/n10 I:http:\/\/example\/fib\/fib L:55\^\^http:\/\/www\.w3\.org\/2001\/XMLSchema#integer@--/);
|
|
271
|
+
assert.equal(result.perRule[0].backward, true);
|
|
272
|
+
assert.equal(result.perRule[1].backward, true);
|
|
273
|
+
assert.ok(result.hybridStats.memoHits > 0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('hybrid query mode runs forward rules with backward body calls', () => {
|
|
277
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
278
|
+
const result = runQuery(`
|
|
279
|
+
PREFIX : <http://example/>
|
|
280
|
+
DATA { :alice :score 41 . :bob :score 2 . }
|
|
281
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
282
|
+
RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
|
|
283
|
+
`, '?who :ready ?score', { queryMode: 'hybrid' });
|
|
284
|
+
assert.equal(result.query.mode, 'hybrid');
|
|
285
|
+
assert.ok(result.hybridStats.goals > 0);
|
|
286
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
287
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
288
|
+
assert.doesNotMatch(output, /bob/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('auto query mode uses pure backward planning when unsupported rules are irrelevant', () => {
|
|
292
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
293
|
+
const result = runQuery(`
|
|
294
|
+
PREFIX : <http://example/>
|
|
295
|
+
DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
|
|
296
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
297
|
+
RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
|
|
298
|
+
RULE { [] :unrelated :x } WHERE { :seed :seen :x }
|
|
299
|
+
`, '?who :ready ?score');
|
|
300
|
+
assert.equal(result.query.mode, 'backward');
|
|
301
|
+
assert.ok(result.query.stats.goals > 0);
|
|
302
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
303
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
304
|
+
assert.doesNotMatch(output, /bob/);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('auto query mode falls back to hybrid when a demanded predicate has unsupported rules', () => {
|
|
308
|
+
const { runQuery, formatBindings } = require('../src/index.js');
|
|
309
|
+
const result = runQuery(`
|
|
310
|
+
PREFIX : <http://example/>
|
|
311
|
+
DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
|
|
312
|
+
RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
|
|
313
|
+
RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
|
|
314
|
+
RULE { [] :ready 999 } WHERE { :seed :seen :x }
|
|
315
|
+
`, '?who :ready ?score');
|
|
316
|
+
assert.equal(result.query.mode, 'hybrid');
|
|
317
|
+
assert.ok(result.hybridStats.goals > 0);
|
|
318
|
+
assert.equal(result.perRule[0].backward, true);
|
|
319
|
+
assert.doesNotMatch(result.closure.map(tripleKey).join('\n'), /nextScore/);
|
|
320
|
+
const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
|
|
321
|
+
assert.match(output, /\?score = 42; \?who = :alice/);
|
|
322
|
+
});
|
|
323
|
+
|
|
162
324
|
test('parseQuery accepts raw body text and rejects non-SRL QUERY/SELECT syntax', () => {
|
|
163
325
|
const { parseQuery } = require('../src/index.js');
|
|
164
326
|
const raw = parseQuery('?x :p ?y', { prefixes: { '': 'http://example/' } });
|
|
@@ -599,4 +761,22 @@ RULE { ?x :p ?v1 } WHERE { ?x :p ?v . SET(?v1 := ?v + 1) }
|
|
|
599
761
|
`, { shacl12Conformance: true, strict: true }), /creates terms in a recursive dependency cycle/);
|
|
600
762
|
});
|
|
601
763
|
|
|
764
|
+
|
|
765
|
+
test('auto backward query ignores unsupported rules outside the demanded slice', () => {
|
|
766
|
+
const source = `
|
|
767
|
+
PREFIX : <http://example/>
|
|
768
|
+
DATA { :alice :parent :bob . :bob :parent :carol . :a :q :b . :b :q :c . }
|
|
769
|
+
RULE { ?x :ancestor ?y } WHERE { ?x :parent ?y }
|
|
770
|
+
RULE { ?x :ancestor ?z } WHERE { ?x :parent ?y . ?y :ancestor ?z }
|
|
771
|
+
RULE { ?x :twoStep ?y } WHERE { ?x :q/:q ?y }
|
|
772
|
+
`;
|
|
773
|
+
const result = runQuery(source, ':alice :ancestor ?who');
|
|
774
|
+
assert.equal(result.query.mode, 'backward');
|
|
775
|
+
assert.ok(result.query.stats.goals > 0);
|
|
776
|
+
assert.deepEqual(
|
|
777
|
+
result.query.bindings.map((binding) => binding.who.value).sort(),
|
|
778
|
+
['http://example/bob', 'http://example/carol'],
|
|
779
|
+
);
|
|
780
|
+
});
|
|
781
|
+
|
|
602
782
|
main();
|
package/test/cli.test.js
CHANGED
|
@@ -22,7 +22,7 @@ function readmeCliOptions() {
|
|
|
22
22
|
test('CLI help documents RDF Message Log flags', () => {
|
|
23
23
|
const text = help();
|
|
24
24
|
assert.match(text, /--rdf-messages\s+Parse input as an RDF Message Log/);
|
|
25
|
-
assert.
|
|
25
|
+
assert.doesNotMatch(text, /--stream-messages/);
|
|
26
26
|
assert.match(text, /--include-message-facts\s+Include payload facts while parsing RDF Message Logs/);
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -34,8 +34,19 @@ test('README CLI options stay in sync with --help', () => {
|
|
|
34
34
|
|
|
35
35
|
test('RDF Message Log flags are accepted by parseArgs', () => {
|
|
36
36
|
assert.equal(parseArgs(['--rdf-messages']).options.rdfMessages, true);
|
|
37
|
-
assert.equal(parseArgs(['--stream-messages']).options.rdfMessages, true);
|
|
38
37
|
assert.equal(parseArgs(['--include-message-facts']).options.includeMessageFacts, true);
|
|
39
38
|
});
|
|
40
39
|
|
|
40
|
+
test('query mode flag is accepted by parseArgs', () => {
|
|
41
|
+
assert.equal(parseArgs(['--query-mode', 'auto']).options.queryMode, 'auto');
|
|
42
|
+
assert.equal(parseArgs(['--query-mode', 'forward']).options.queryMode, 'forward');
|
|
43
|
+
assert.equal(parseArgs(['--query-mode', 'backward']).options.queryMode, 'backward');
|
|
44
|
+
assert.equal(parseArgs([]).options.hybrid, 'auto');
|
|
45
|
+
assert.equal(parseArgs(['--hybrid']).options.hybrid, true);
|
|
46
|
+
assert.equal(parseArgs(['--no-hybrid']).options.hybrid, false);
|
|
47
|
+
assert.throws(() => parseArgs(['--query-mode', 'hybrid']), /--query-mode requires auto, forward, or backward/);
|
|
48
|
+
assert.throws(() => parseArgs(['--query-mode', 'sideways']), /--query-mode requires auto, forward, or backward/);
|
|
49
|
+
assert.throws(() => parseArgs(['--stream-messages']), /Unknown option --stream-messages/);
|
|
50
|
+
});
|
|
51
|
+
|
|
41
52
|
main();
|
package/test/perf-baseline.json
CHANGED
|
@@ -11,28 +11,28 @@
|
|
|
11
11
|
"name": "deep-taxonomy-100000.srl",
|
|
12
12
|
"file": "examples/deep-taxonomy-100000.srl",
|
|
13
13
|
"repeat": 1,
|
|
14
|
-
"baselineMs":
|
|
14
|
+
"baselineMs": 12316.7,
|
|
15
15
|
"outputBytes": 5068161
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"name": "fibonacci.srl",
|
|
19
19
|
"file": "examples/fibonacci.srl",
|
|
20
20
|
"repeat": 1,
|
|
21
|
-
"baselineMs":
|
|
22
|
-
"outputBytes":
|
|
21
|
+
"baselineMs": 124.0,
|
|
22
|
+
"outputBytes": 2405
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"name": "fft32-numeric.srl",
|
|
26
26
|
"file": "examples/fft32-numeric.srl",
|
|
27
|
-
"repeat":
|
|
28
|
-
"baselineMs":
|
|
27
|
+
"repeat": 1,
|
|
28
|
+
"baselineMs": 607.8,
|
|
29
29
|
"outputBytes": 466351
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"name": "path-discovery.srl",
|
|
33
33
|
"file": "examples/path-discovery.srl",
|
|
34
|
-
"repeat":
|
|
35
|
-
"baselineMs":
|
|
34
|
+
"repeat": 1,
|
|
35
|
+
"baselineMs": 3054.6,
|
|
36
36
|
"outputBytes": 736870
|
|
37
37
|
}
|
|
38
38
|
]
|