eyeleng 1.0.13 → 1.1.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 +22 -5
- package/dist/browser/eyeleng.browser.js +637 -14
- package/eyeleng.js +685 -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 +490 -0
- package/src/cli.js +48 -10
- package/src/engine.js +68 -5
- package/src/query.js +69 -7
- package/src/store.js +2 -0
- package/test/api.test.js +127 -1
- package/test/cli.test.js +11 -2
package/src/api.js
CHANGED
|
@@ -6,7 +6,7 @@ const { parseRdfMessageLog, looksLikeRdfMessageLog } = require('./rdfMessages.js
|
|
|
6
6
|
const { evaluate } = require('./engine.js');
|
|
7
7
|
const { analyze } = require('./analyze.js');
|
|
8
8
|
const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
|
|
9
|
-
const { runQuery, queryResult } = require('./query.js');
|
|
9
|
+
const { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');
|
|
10
10
|
const { resultTriples } = require('./output.js');
|
|
11
11
|
|
|
12
12
|
function parseInput(source, options = {}) {
|
|
@@ -120,6 +120,9 @@ module.exports = {
|
|
|
120
120
|
runToString,
|
|
121
121
|
runQuery,
|
|
122
122
|
queryResult,
|
|
123
|
+
queryProgram,
|
|
124
|
+
queryRunOptions,
|
|
125
|
+
shouldUseHybridForQuery,
|
|
123
126
|
formatTriples,
|
|
124
127
|
formatBindings,
|
|
125
128
|
sortTriples,
|
package/src/backward.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { TripleStore, bindingKey } = require('./store.js');
|
|
4
|
+
const { tripleKey, termKey, termEquals } = require('./term.js');
|
|
5
|
+
const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
|
|
6
|
+
|
|
7
|
+
function backwardQuery(program, querySpec, options = {}) {
|
|
8
|
+
const planner = planBackwardQuery(program, querySpec, options);
|
|
9
|
+
if (!planner.ok) return { ok: false, reason: planner.reason };
|
|
10
|
+
const prover = new BackwardProver(program, { ...options, allowedRuleIndexes: planner.ruleIndexes });
|
|
11
|
+
const bindings = uniqueBindings(Array.from(prover.solveBody(querySpec.body, {})));
|
|
12
|
+
return { ok: true, bindings, stats: prover.stats, plan: planner };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function planBackwardQuery(program, querySpec, options = {}) {
|
|
16
|
+
const clauses = querySpec && Array.isArray(querySpec.body) ? querySpec.body : [];
|
|
17
|
+
if (!bodySupported(clauses, options)) return { ok: false, reason: 'query body contains clauses not supported by the backward prover yet' };
|
|
18
|
+
|
|
19
|
+
const reachable = reachableBackwardRuleIndexes(program, clauses, options);
|
|
20
|
+
for (const ruleIndex of reachable.ruleIndexes) {
|
|
21
|
+
const rule = (program.rules || [])[ruleIndex];
|
|
22
|
+
if (!ruleSupported(rule, options)) return { ok: false, reason: `reachable rule ${rule.name || '<anonymous>'} is not supported by the backward prover yet` };
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, ruleIndexes: reachable.ruleIndexes, predicates: reachable.predicates };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bodySupported(clauses, options = {}) {
|
|
28
|
+
for (const clause of clauses || []) {
|
|
29
|
+
if (clause.type === 'triple' || clause.type === 'filter' || clause.type === 'set' || clause.type === 'bind') continue;
|
|
30
|
+
if (clause.type === 'not') {
|
|
31
|
+
if (options.backwardNegation === false) return false;
|
|
32
|
+
if (!bodySupported(clause.body, options)) return false;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ruleSupported(rule, options = {}) {
|
|
41
|
+
if (!Array.isArray(rule.head) || rule.head.length === 0) return false;
|
|
42
|
+
for (const head of rule.head) {
|
|
43
|
+
if (!head || !head.p || head.p.type !== 'iri') return false;
|
|
44
|
+
if (containsBlank(head.s) || containsBlank(head.p) || containsBlank(head.o)) return false;
|
|
45
|
+
}
|
|
46
|
+
return bodySupported(rule.body || [], options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class BackwardProver {
|
|
50
|
+
constructor(program, options = {}) {
|
|
51
|
+
this.program = program;
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.store = options.store || new TripleStore(program.data || []);
|
|
54
|
+
this.maxDepth = options.backwardMaxDepth || options.maxDepth || 10000;
|
|
55
|
+
this.solutionLimit = options.backwardSolutionLimit || options.solutionLimit || 1000000;
|
|
56
|
+
this.allowedPredicates = normalizePredicateSet(options.allowedPredicates || options.backwardPredicates || null);
|
|
57
|
+
this.allowedRuleIndexes = normalizeRuleIndexSet(options.allowedRuleIndexes || null);
|
|
58
|
+
this.ruleHeads = indexRuleHeads(program.rules || [], { allowedPredicates: this.allowedPredicates, allowedRuleIndexes: this.allowedRuleIndexes });
|
|
59
|
+
this.memo = new Map();
|
|
60
|
+
this.active = new Set();
|
|
61
|
+
this.freshCounter = 0;
|
|
62
|
+
this.solutionCount = 0;
|
|
63
|
+
this.stats = {
|
|
64
|
+
mode: 'backward',
|
|
65
|
+
goals: 0,
|
|
66
|
+
facts: 0,
|
|
67
|
+
rules: 0,
|
|
68
|
+
memoHits: 0,
|
|
69
|
+
memoStores: 0,
|
|
70
|
+
maxDepth: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
*solveBody(clauses, binding = {}, depth = 0, index = 0) {
|
|
75
|
+
if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);
|
|
76
|
+
this.stats.maxDepth = Math.max(this.stats.maxDepth, depth);
|
|
77
|
+
if (this.solutionCount >= this.solutionLimit) return;
|
|
78
|
+
if (index >= clauses.length) {
|
|
79
|
+
this.solutionCount += 1;
|
|
80
|
+
yield resolveBinding(binding);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const clause = clauses[index];
|
|
85
|
+
if (clause.type === 'triple') {
|
|
86
|
+
for (const matched of this.solveTriple(clause.triple, binding, depth + 1)) {
|
|
87
|
+
yield* this.solveBody(clauses, matched, depth + 1, index + 1);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (clause.type === 'filter') {
|
|
93
|
+
try {
|
|
94
|
+
if (booleanValue(evalExpression(clause.expr, resolveBinding(binding), this.options))) {
|
|
95
|
+
yield* this.solveBody(clauses, binding, depth + 1, index + 1);
|
|
96
|
+
}
|
|
97
|
+
} catch (_) {
|
|
98
|
+
// SPARQL-style FILTER errors reject the current solution.
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (clause.type === 'set' || clause.type === 'bind') {
|
|
104
|
+
try {
|
|
105
|
+
const resolved = resolveBinding(binding);
|
|
106
|
+
const value = asTerm(evalExpression(clause.expr, resolved, this.options));
|
|
107
|
+
const next = unifyTerms({ type: 'var', value: clause.variable }, value, binding);
|
|
108
|
+
if (next) yield* this.solveBody(clauses, next, depth + 1, index + 1);
|
|
109
|
+
} catch (_) {
|
|
110
|
+
// Assignment errors drop the current solution.
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (clause.type === 'not') {
|
|
116
|
+
let found = false;
|
|
117
|
+
for (const _ of this.solveBody(clause.body, { ...binding }, depth + 1, 0)) { found = true; break; }
|
|
118
|
+
if (!found) yield* this.solveBody(clauses, binding, depth + 1, index + 1);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(`Unsupported backward body clause ${clause.type}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
*solveTriple(pattern, binding = {}, depth = 0) {
|
|
126
|
+
if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);
|
|
127
|
+
this.stats.goals += 1;
|
|
128
|
+
const resolvedPattern = resolvePattern(pattern, binding);
|
|
129
|
+
const key = `${goalKey(resolvedPattern)}@store:${this.store.version || 0}`;
|
|
130
|
+
const entry = this.memo.get(key);
|
|
131
|
+
if (entry && entry.complete) {
|
|
132
|
+
this.stats.memoHits += 1;
|
|
133
|
+
for (const answer of entry.answers) {
|
|
134
|
+
const next = unifyTriples(pattern, answer, binding);
|
|
135
|
+
if (next) yield next;
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (this.active.has(key)) return;
|
|
140
|
+
|
|
141
|
+
const answers = [];
|
|
142
|
+
const answerKeys = new Set();
|
|
143
|
+
this.active.add(key);
|
|
144
|
+
try {
|
|
145
|
+
for (const fact of this.factCandidates(resolvedPattern, binding)) {
|
|
146
|
+
const next = unifyTriples(pattern, fact, binding);
|
|
147
|
+
if (!next) continue;
|
|
148
|
+
rememberAnswer(answers, answerKeys, pattern, next);
|
|
149
|
+
this.stats.facts += 1;
|
|
150
|
+
yield next;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const item of this.ruleCandidates(resolvedPattern)) {
|
|
154
|
+
const suffix = `__b${++this.freshCounter}_${item.ruleIndex}`;
|
|
155
|
+
const freshHead = freshTriple(item.head, suffix);
|
|
156
|
+
const next = unifyTriples(pattern, freshHead, binding);
|
|
157
|
+
if (!next) continue;
|
|
158
|
+
const freshBody = (item.rule.body || []).map((clause) => freshClause(clause, suffix));
|
|
159
|
+
for (const solved of this.solveBody(freshBody, next, depth + 1, 0)) {
|
|
160
|
+
rememberAnswer(answers, answerKeys, pattern, solved);
|
|
161
|
+
this.stats.rules += 1;
|
|
162
|
+
yield solved;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
this.active.delete(key);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.memo.set(key, { complete: true, answers });
|
|
170
|
+
this.stats.memoStores += 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
factCandidates(pattern, binding) {
|
|
174
|
+
return this.store.candidates(pattern, binding);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ruleCandidates(pattern) {
|
|
178
|
+
const predicate = pattern.p && pattern.p.type === 'iri' ? pattern.p.value : null;
|
|
179
|
+
if (predicate) return this.ruleHeads.byPredicate.get(predicate) || [];
|
|
180
|
+
return this.ruleHeads.all;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function indexRuleHeads(rules, options = {}) {
|
|
185
|
+
const allowedPredicates = options.allowedPredicates || null;
|
|
186
|
+
const allowedRuleIndexes = options.allowedRuleIndexes || null;
|
|
187
|
+
const byPredicate = new Map();
|
|
188
|
+
const all = [];
|
|
189
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
190
|
+
if (allowedRuleIndexes && !allowedRuleIndexes.has(ruleIndex)) continue;
|
|
191
|
+
const rule = rules[ruleIndex];
|
|
192
|
+
for (let headIndex = 0; headIndex < (rule.head || []).length; headIndex += 1) {
|
|
193
|
+
const head = rule.head[headIndex];
|
|
194
|
+
if (!head || !head.p || head.p.type !== 'iri') continue;
|
|
195
|
+
if (allowedPredicates && !allowedPredicates.has(head.p.value)) continue;
|
|
196
|
+
const item = { ruleIndex, headIndex, rule, head };
|
|
197
|
+
all.push(item);
|
|
198
|
+
const bucket = byPredicate.get(head.p.value);
|
|
199
|
+
if (bucket) bucket.push(item);
|
|
200
|
+
else byPredicate.set(head.p.value, [item]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { byPredicate, all };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function freshClause(clause, suffix) {
|
|
207
|
+
if (clause.type === 'triple') return { ...clause, triple: freshTriple(clause.triple, suffix) };
|
|
208
|
+
if (clause.type === 'filter') return { ...clause, expr: freshExpr(clause.expr, suffix) };
|
|
209
|
+
if (clause.type === 'set' || clause.type === 'bind') return { ...clause, variable: freshVarName(clause.variable, suffix), expr: freshExpr(clause.expr, suffix) };
|
|
210
|
+
if (clause.type === 'not') return { ...clause, body: clause.body.map((item) => freshClause(item, suffix)) };
|
|
211
|
+
return clause;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function freshTriple(triple, suffix) {
|
|
215
|
+
return { s: freshTerm(triple.s, suffix), p: freshTerm(triple.p, suffix), o: freshTerm(triple.o, suffix) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function freshTerm(term, suffix) {
|
|
219
|
+
if (!term) return term;
|
|
220
|
+
if (term.type === 'var') return { type: 'var', value: freshVarName(term.value, suffix) };
|
|
221
|
+
if (term.type === 'triple') return { type: 'triple', s: freshTerm(term.s, suffix), p: freshTerm(term.p, suffix), o: freshTerm(term.o, suffix) };
|
|
222
|
+
return term;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function freshExpr(expr, suffix) {
|
|
226
|
+
if (!expr) return expr;
|
|
227
|
+
if (expr.type === 'var') return { ...expr, name: freshVarName(expr.name, suffix) };
|
|
228
|
+
if (expr.type === 'term') return { ...expr, value: freshTerm(expr.value, suffix) };
|
|
229
|
+
if (expr.type === 'unary') return { ...expr, expr: freshExpr(expr.expr, suffix) };
|
|
230
|
+
if (expr.type === 'binary') return { ...expr, left: freshExpr(expr.left, suffix), right: freshExpr(expr.right, suffix) };
|
|
231
|
+
if (expr.type === 'call') return { ...expr, args: expr.args.map((arg) => freshExpr(arg, suffix)) };
|
|
232
|
+
if (expr.type === 'list') return { ...expr, items: expr.items.map((item) => freshExpr(item, suffix)) };
|
|
233
|
+
return expr;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function freshVarName(name, suffix) {
|
|
237
|
+
return `${name}${suffix}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resolvePattern(pattern, binding) {
|
|
241
|
+
return { s: resolveTerm(pattern.s, binding, false), p: resolveTerm(pattern.p, binding, false), o: resolveTerm(pattern.o, binding, false) };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveBinding(binding) {
|
|
245
|
+
const out = {};
|
|
246
|
+
for (const name of Object.keys(binding)) out[name] = resolveTerm(binding[name], binding, false);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveTerm(term, binding, preserveUnbound = true, seen = new Set()) {
|
|
251
|
+
if (!term) return term;
|
|
252
|
+
if (term.type === 'var') {
|
|
253
|
+
const name = term.value;
|
|
254
|
+
if (seen.has(name)) return preserveUnbound ? term : { type: 'var', value: name };
|
|
255
|
+
if (!Object.prototype.hasOwnProperty.call(binding, name)) return preserveUnbound ? term : { type: 'var', value: name };
|
|
256
|
+
seen.add(name);
|
|
257
|
+
return resolveTerm(binding[name], binding, preserveUnbound, seen);
|
|
258
|
+
}
|
|
259
|
+
if (term.type === 'triple') {
|
|
260
|
+
return {
|
|
261
|
+
type: 'triple',
|
|
262
|
+
s: resolveTerm(term.s, binding, preserveUnbound, new Set(seen)),
|
|
263
|
+
p: resolveTerm(term.p, binding, preserveUnbound, new Set(seen)),
|
|
264
|
+
o: resolveTerm(term.o, binding, preserveUnbound, new Set(seen)),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return term;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function unifyTriples(left, right, binding) {
|
|
271
|
+
let next = unifyTerms(left.s, right.s, binding);
|
|
272
|
+
if (!next) return null;
|
|
273
|
+
next = unifyTerms(left.p, right.p, next);
|
|
274
|
+
if (!next) return null;
|
|
275
|
+
return unifyTerms(left.o, right.o, next);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function unifyTerms(left, right, binding) {
|
|
279
|
+
const a = resolveTerm(left, binding);
|
|
280
|
+
const b = resolveTerm(right, binding);
|
|
281
|
+
if (a.type === 'var' && b.type === 'var' && a.value === b.value) return binding;
|
|
282
|
+
if (a.type === 'var') return bindVariable(a.value, b, binding);
|
|
283
|
+
if (b.type === 'var') return bindVariable(b.value, a, binding);
|
|
284
|
+
if (a.type === 'triple' || b.type === 'triple') {
|
|
285
|
+
if (a.type !== 'triple' || b.type !== 'triple') return null;
|
|
286
|
+
let next = unifyTerms(a.s, b.s, binding);
|
|
287
|
+
if (!next) return null;
|
|
288
|
+
next = unifyTerms(a.p, b.p, next);
|
|
289
|
+
if (!next) return null;
|
|
290
|
+
return unifyTerms(a.o, b.o, next);
|
|
291
|
+
}
|
|
292
|
+
return termEquals(a, b) ? binding : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function bindVariable(name, term, binding) {
|
|
296
|
+
const existing = binding[name];
|
|
297
|
+
if (existing) return unifyTerms(existing, term, binding);
|
|
298
|
+
if (term.type === 'var' && term.value === name) return binding;
|
|
299
|
+
return { ...binding, [name]: term };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function rememberAnswer(answers, answerKeys, pattern, binding) {
|
|
303
|
+
const triple = {
|
|
304
|
+
s: resolveTerm(pattern.s, binding),
|
|
305
|
+
p: resolveTerm(pattern.p, binding),
|
|
306
|
+
o: resolveTerm(pattern.o, binding),
|
|
307
|
+
};
|
|
308
|
+
if (triple.s.type === 'var' || triple.p.type === 'var' || triple.o.type === 'var') return;
|
|
309
|
+
const key = tripleKey(triple);
|
|
310
|
+
if (answerKeys.has(key)) return;
|
|
311
|
+
answerKeys.add(key);
|
|
312
|
+
answers.push(triple);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function goalKey(pattern) {
|
|
316
|
+
return `${safeTermKey(pattern.s)} ${safeTermKey(pattern.p)} ${safeTermKey(pattern.o)}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function safeTermKey(term) {
|
|
320
|
+
return term && term.type === 'var' ? '_' : termKey(term);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function uniqueBindings(bindings) {
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const binding of bindings) {
|
|
327
|
+
const resolved = resolveBinding(binding);
|
|
328
|
+
const key = bindingKey(resolved);
|
|
329
|
+
if (seen.has(key)) continue;
|
|
330
|
+
seen.add(key);
|
|
331
|
+
out.push(resolved);
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function containsBlank(term) {
|
|
337
|
+
if (!term) return false;
|
|
338
|
+
if (term.type === 'blank') return true;
|
|
339
|
+
if (term.type === 'triple') return containsBlank(term.s) || containsBlank(term.p) || containsBlank(term.o);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
function reachableBackwardRuleIndexes(program, rootClauses, options = {}) {
|
|
346
|
+
const rules = program.rules || [];
|
|
347
|
+
const headIndex = new Map();
|
|
348
|
+
const allHeadPredicates = new Set();
|
|
349
|
+
const allRuleIndexes = new Set();
|
|
350
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
351
|
+
const rule = rules[ruleIndex];
|
|
352
|
+
for (const head of rule.head || []) {
|
|
353
|
+
if (!head || !head.p || head.p.type !== 'iri') continue;
|
|
354
|
+
allRuleIndexes.add(ruleIndex);
|
|
355
|
+
allHeadPredicates.add(head.p.value);
|
|
356
|
+
if (!headIndex.has(head.p.value)) headIndex.set(head.p.value, new Set());
|
|
357
|
+
headIndex.get(head.p.value).add(ruleIndex);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const predicates = new Set();
|
|
362
|
+
const ruleIndexes = new Set();
|
|
363
|
+
const work = [];
|
|
364
|
+
const enqueue = (predicate) => {
|
|
365
|
+
if (!predicate) {
|
|
366
|
+
for (const item of allHeadPredicates) enqueue(item);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (predicates.has(predicate)) return;
|
|
370
|
+
predicates.add(predicate);
|
|
371
|
+
work.push(predicate);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
for (const predicate of bodyPredicateDemands(rootClauses || [])) enqueue(predicate);
|
|
375
|
+
|
|
376
|
+
while (work.length > 0) {
|
|
377
|
+
const predicate = work.shift();
|
|
378
|
+
const indexes = headIndex.get(predicate);
|
|
379
|
+
if (!indexes) continue;
|
|
380
|
+
for (const ruleIndex of indexes) {
|
|
381
|
+
if (ruleIndexes.has(ruleIndex)) continue;
|
|
382
|
+
ruleIndexes.add(ruleIndex);
|
|
383
|
+
const rule = rules[ruleIndex];
|
|
384
|
+
for (const needed of bodyPredicateDemands(rule.body || [])) enqueue(needed);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { ruleIndexes, predicates };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function bodyPredicateDemands(clauses) {
|
|
392
|
+
const out = [];
|
|
393
|
+
for (const clause of clauses || []) {
|
|
394
|
+
if (clause.type === 'triple') {
|
|
395
|
+
out.push(predicateDemand(clause.triple.p));
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (clause.type === 'not') {
|
|
399
|
+
out.push(...bodyPredicateDemands(clause.body || []));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function predicateDemand(term) {
|
|
406
|
+
return term && term.type === 'iri' ? term.value : null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normalizePredicateSet(value) {
|
|
410
|
+
if (!value) return null;
|
|
411
|
+
if (value instanceof Set) return value;
|
|
412
|
+
if (Array.isArray(value)) return new Set(value);
|
|
413
|
+
return new Set(String(value).split(',').map((item) => item.trim()).filter(Boolean));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function normalizeRuleIndexSet(value) {
|
|
417
|
+
if (!value) return null;
|
|
418
|
+
if (value instanceof Set) return value;
|
|
419
|
+
if (Array.isArray(value)) return new Set(value);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function supportedBackwardPredicates(program, options = {}) {
|
|
424
|
+
const explicit = normalizePredicateSet(options.backwardPredicates || options.hybridPredicates || null);
|
|
425
|
+
const byPredicate = new Map();
|
|
426
|
+
for (const rule of program.rules || []) {
|
|
427
|
+
const predicates = new Set();
|
|
428
|
+
for (const head of rule.head || []) {
|
|
429
|
+
if (head && head.p && head.p.type === 'iri') predicates.add(head.p.value);
|
|
430
|
+
}
|
|
431
|
+
for (const predicate of predicates) {
|
|
432
|
+
if (!byPredicate.has(predicate)) byPredicate.set(predicate, []);
|
|
433
|
+
byPredicate.get(predicate).push(rule);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const supported = new Set();
|
|
438
|
+
for (const [predicate, rules] of byPredicate) {
|
|
439
|
+
if (explicit && !explicit.has(predicate)) continue;
|
|
440
|
+
if (rules.length > 0 && rules.every((rule) => ruleSupported(rule, options))) supported.add(predicate);
|
|
441
|
+
}
|
|
442
|
+
return supported;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function preferredBackwardPredicates(program, options = {}) {
|
|
446
|
+
const explicit = normalizePredicateSet(options.hybridPredicates || null);
|
|
447
|
+
if (explicit) return supportedBackwardPredicates(program, { ...options, hybridPredicates: explicit });
|
|
448
|
+
const supported = supportedBackwardPredicates(program, options);
|
|
449
|
+
const preferred = new Set();
|
|
450
|
+
for (const rule of program.rules || []) {
|
|
451
|
+
if (!ruleIsFunctionLike(rule)) continue;
|
|
452
|
+
for (const head of rule.head || []) {
|
|
453
|
+
if (head && head.p && head.p.type === 'iri' && supported.has(head.p.value)) preferred.add(head.p.value);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return preferred;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function ruleIsFunctionLike(rule) {
|
|
460
|
+
return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function ruleHeadPredicates(rule) {
|
|
464
|
+
const predicates = new Set();
|
|
465
|
+
for (const head of rule.head || []) {
|
|
466
|
+
if (!head || !head.p || head.p.type !== 'iri') return null;
|
|
467
|
+
predicates.add(head.p.value);
|
|
468
|
+
}
|
|
469
|
+
return predicates;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function ruleIsBackwardOriented(rule, predicates) {
|
|
473
|
+
if (!predicates || predicates.size === 0) return false;
|
|
474
|
+
const heads = ruleHeadPredicates(rule);
|
|
475
|
+
if (!heads || heads.size === 0) return false;
|
|
476
|
+
for (const predicate of heads) if (!predicates.has(predicate)) return false;
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
module.exports = {
|
|
481
|
+
BackwardProver,
|
|
482
|
+
backwardQuery,
|
|
483
|
+
planBackwardQuery,
|
|
484
|
+
supportedBackwardPredicates,
|
|
485
|
+
preferredBackwardPredicates,
|
|
486
|
+
reachableBackwardRuleIndexes,
|
|
487
|
+
ruleIsBackwardOriented,
|
|
488
|
+
ruleSupported,
|
|
489
|
+
resolveBinding,
|
|
490
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -8,6 +8,8 @@ const {
|
|
|
8
8
|
evaluate,
|
|
9
9
|
parseQuery,
|
|
10
10
|
queryResult,
|
|
11
|
+
queryProgram,
|
|
12
|
+
queryRunOptions,
|
|
11
13
|
formatTriples,
|
|
12
14
|
formatTrace,
|
|
13
15
|
formatBindings,
|
|
@@ -34,7 +36,7 @@ function readPackageVersion() {
|
|
|
34
36
|
const VERSION = readPackageVersion();
|
|
35
37
|
|
|
36
38
|
function help() {
|
|
37
|
-
return `eyeleng ${VERSION}\n\nA dependency-free JavaScript implementation experiment for the SHACL 1.2 Rules draft, including SRL and RDF Rules syntax front-ends.\n\nUsage:\n eyeleng [options] [file ...]\n\nOptions:\n --all Print the full closure, including input facts\n --json Print JSON instead of compact triples/bindings\n --trace Print derivation trace to stderr, or include it in JSON\n --stats Print iteration and triple counts to stderr\n --check Parse and analyze only; do not run rules\n --strict Treat static warnings as errors, including recursive term generation\n --deps Print rule dependency edges during --check\n --query TEXT Run a raw SRL body pattern over the closure\n --query-file FILE Read a raw SRL body pattern from a file\n --max-iterations N Stop after N fixpoint iterations within a recursive layer\n --no-imports Parse IMPORTS/owl:imports but do not load imported rule sets\n --rdf-messages Parse input as an RDF Message Log\n --
|
|
39
|
+
return `eyeleng ${VERSION}\n\nA dependency-free JavaScript implementation experiment for the SHACL 1.2 Rules draft, including SRL and RDF Rules syntax front-ends.\n\nUsage:\n eyeleng [options] [file ...]\n\nOptions:\n --all Print the full closure, including input facts\n --json Print JSON instead of compact triples/bindings\n --trace Print derivation trace to stderr, or include it in JSON\n --stats Print iteration and triple counts to stderr\n --check Parse and analyze only; do not run rules\n --strict Treat static warnings as errors, including recursive term generation\n --deps Print rule dependency edges during --check\n --query TEXT Run a raw SRL body pattern over the closure or backward planner\n --query-file FILE Read a raw SRL body pattern from a file\n --query-mode MODE Use auto, forward, or backward query planning (default auto)\n --hybrid Orient function-like rules backward during forward execution and queries\n --max-iterations N Stop after N fixpoint iterations within a recursive layer\n --no-imports Parse IMPORTS/owl:imports but do not load imported rule sets\n --rdf-messages Parse input as an RDF Message Log\n --include-message-facts Include payload facts while parsing RDF Message Logs\n --syntax MODE Use srl, rdf, or auto syntax detection (default auto)\n --ruleset TERM In RDF syntax, run only the selected srl:RuleSet\n --version Print version\n -h, --help Print this help\n\nWith no file arguments, eyeleng reads from stdin.\n`;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
function parseArgs(argv) {
|
|
@@ -48,6 +50,8 @@ function parseArgs(argv) {
|
|
|
48
50
|
deps: false,
|
|
49
51
|
query: null,
|
|
50
52
|
queryFile: null,
|
|
53
|
+
queryMode: 'auto',
|
|
54
|
+
hybrid: false,
|
|
51
55
|
maxIterations: 10000,
|
|
52
56
|
imports: true,
|
|
53
57
|
syntax: 'auto',
|
|
@@ -66,7 +70,8 @@ function parseArgs(argv) {
|
|
|
66
70
|
else if (arg === '--strict') options.strict = true;
|
|
67
71
|
else if (arg === '--deps') options.deps = true;
|
|
68
72
|
else if (arg === '--no-imports') options.imports = false;
|
|
69
|
-
else if (arg === '--
|
|
73
|
+
else if (arg === '--hybrid') options.hybrid = true;
|
|
74
|
+
else if (arg === '--rdf-messages') options.rdfMessages = true;
|
|
70
75
|
else if (arg === '--include-message-facts') options.includeMessageFacts = true;
|
|
71
76
|
else if (arg === '--syntax') {
|
|
72
77
|
i += 1;
|
|
@@ -77,8 +82,12 @@ function parseArgs(argv) {
|
|
|
77
82
|
i += 1;
|
|
78
83
|
if (i >= argv.length) throw new Error('--ruleset requires an RDF term');
|
|
79
84
|
options.ruleSet = argv[i];
|
|
80
|
-
}
|
|
81
|
-
|
|
85
|
+
} else if (arg === '--query-mode') {
|
|
86
|
+
i += 1;
|
|
87
|
+
if (i >= argv.length) throw new Error('--query-mode requires auto, forward, or backward');
|
|
88
|
+
options.queryMode = argv[i];
|
|
89
|
+
if (!['auto', 'forward', 'backward'].includes(options.queryMode)) throw new Error('--query-mode requires auto, forward, or backward');
|
|
90
|
+
} else if (arg === '--query') {
|
|
82
91
|
i += 1;
|
|
83
92
|
if (i >= argv.length) throw new Error('--query requires a value');
|
|
84
93
|
options.query = argv[i];
|
|
@@ -191,15 +200,42 @@ function main(argv = process.argv.slice(2), io = process) {
|
|
|
191
200
|
}
|
|
192
201
|
if (fatal) return 1;
|
|
193
202
|
|
|
194
|
-
const result = evaluate(compiled.program, { ...options, analysis: compiled.analysis });
|
|
195
|
-
result.diagnostics = compiled.diagnostics;
|
|
196
|
-
result.analysis = compiled.analysis;
|
|
197
|
-
|
|
198
203
|
const queryText = options.queryFile ? fs.readFileSync(options.queryFile, 'utf8') : options.query;
|
|
199
204
|
const querySpec = queryText
|
|
200
205
|
? parseQuery(queryText, { filename: options.queryFile || '<query>', prefixes: compiled.program.prefixes, baseIRI: compiled.program.baseIRI })
|
|
201
206
|
: null;
|
|
202
|
-
|
|
207
|
+
|
|
208
|
+
let result;
|
|
209
|
+
if (querySpec) {
|
|
210
|
+
const directQuery = queryProgram(compiled.program, querySpec, options);
|
|
211
|
+
if (directQuery) {
|
|
212
|
+
result = {
|
|
213
|
+
baseIRI: compiled.program.baseIRI,
|
|
214
|
+
prefixes: compiled.program.prefixes,
|
|
215
|
+
input: compiled.program.data.slice(),
|
|
216
|
+
inferred: [],
|
|
217
|
+
closure: compiled.program.data.slice(),
|
|
218
|
+
iterations: 0,
|
|
219
|
+
layers: [],
|
|
220
|
+
ruleApplications: 0,
|
|
221
|
+
perRule: [],
|
|
222
|
+
trace: [],
|
|
223
|
+
diagnostics: compiled.diagnostics,
|
|
224
|
+
analysis: compiled.analysis,
|
|
225
|
+
query: directQuery,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!result) {
|
|
231
|
+
const runOptions = querySpec
|
|
232
|
+
? queryRunOptions(compiled.program, querySpec, options)
|
|
233
|
+
: { ...options, hybrid: options.hybrid };
|
|
234
|
+
result = evaluate(compiled.program, { ...runOptions, analysis: compiled.analysis });
|
|
235
|
+
result.diagnostics = compiled.diagnostics;
|
|
236
|
+
result.analysis = compiled.analysis;
|
|
237
|
+
if (querySpec) result.query = queryResult(result, querySpec, runOptions);
|
|
238
|
+
}
|
|
203
239
|
|
|
204
240
|
if (options.json) {
|
|
205
241
|
io.stdout.write(`${JSON.stringify(toJSON(result, { all: options.all, trace: options.trace, analysis: options.deps }), null, 2)}\n`);
|
|
@@ -214,7 +250,9 @@ function main(argv = process.argv.slice(2), io = process) {
|
|
|
214
250
|
}
|
|
215
251
|
|
|
216
252
|
if (options.stats) {
|
|
217
|
-
io.stderr.write(`eyeleng: iterations=${result.iterations} layers=${result.layers.length} input=${result.input.length} inferred=${result.inferred.length} closure=${result.closure.length} ruleApplications=${result.ruleApplications}\n`);
|
|
253
|
+
io.stderr.write(`eyeleng: iterations=${result.iterations} layers=${result.layers.length} input=${result.input.length} inferred=${result.inferred.length} closure=${result.closure.length} ruleApplications=${result.ruleApplications}${result.query && result.query.mode ? ` queryMode=${result.query.mode}` : ''}\n`);
|
|
254
|
+
if (result.query && result.query.stats) io.stderr.write(`eyeleng: backward goals=${result.query.stats.goals} facts=${result.query.stats.facts} rules=${result.query.stats.rules} memoHits=${result.query.stats.memoHits} memoStores=${result.query.stats.memoStores}\n`);
|
|
255
|
+
if (result.hybridStats) io.stderr.write(`eyeleng: hybridBackward goals=${result.hybridStats.goals} facts=${result.hybridStats.facts} rules=${result.hybridStats.rules} memoHits=${result.hybridStats.memoHits} memoStores=${result.hybridStats.memoStores}\n`);
|
|
218
256
|
for (const rule of result.perRule) {
|
|
219
257
|
if (rule.applications > 0 || rule.added > 0) io.stderr.write(`eyeleng: rule ${rule.name}: applications=${rule.applications} added=${rule.added}${rule.runOnce ? ' runOnce=true' : ''}\n`);
|
|
220
258
|
}
|