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/eyeleng.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
evaluate,
|
|
15
15
|
parseQuery,
|
|
16
16
|
queryResult,
|
|
17
|
+
queryProgram,
|
|
18
|
+
queryRunOptions,
|
|
17
19
|
formatTriples,
|
|
18
20
|
formatTrace,
|
|
19
21
|
formatBindings,
|
|
@@ -40,7 +42,7 @@
|
|
|
40
42
|
const VERSION = readPackageVersion();
|
|
41
43
|
|
|
42
44
|
function help() {
|
|
43
|
-
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 --
|
|
45
|
+
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 Force aggressive hybrid orientation for function-like rules\n --no-hybrid Disable automatic hybrid forward/backward execution\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`;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function parseArgs(argv) {
|
|
@@ -54,6 +56,8 @@
|
|
|
54
56
|
deps: false,
|
|
55
57
|
query: null,
|
|
56
58
|
queryFile: null,
|
|
59
|
+
queryMode: 'auto',
|
|
60
|
+
hybrid: 'auto',
|
|
57
61
|
maxIterations: 10000,
|
|
58
62
|
imports: true,
|
|
59
63
|
syntax: 'auto',
|
|
@@ -72,7 +76,9 @@
|
|
|
72
76
|
else if (arg === '--strict') options.strict = true;
|
|
73
77
|
else if (arg === '--deps') options.deps = true;
|
|
74
78
|
else if (arg === '--no-imports') options.imports = false;
|
|
75
|
-
else if (arg === '--
|
|
79
|
+
else if (arg === '--hybrid') options.hybrid = true;
|
|
80
|
+
else if (arg === '--no-hybrid') options.hybrid = false;
|
|
81
|
+
else if (arg === '--rdf-messages') options.rdfMessages = true;
|
|
76
82
|
else if (arg === '--include-message-facts') options.includeMessageFacts = true;
|
|
77
83
|
else if (arg === '--syntax') {
|
|
78
84
|
i += 1;
|
|
@@ -83,8 +89,12 @@
|
|
|
83
89
|
i += 1;
|
|
84
90
|
if (i >= argv.length) throw new Error('--ruleset requires an RDF term');
|
|
85
91
|
options.ruleSet = argv[i];
|
|
86
|
-
}
|
|
87
|
-
|
|
92
|
+
} else if (arg === '--query-mode') {
|
|
93
|
+
i += 1;
|
|
94
|
+
if (i >= argv.length) throw new Error('--query-mode requires auto, forward, or backward');
|
|
95
|
+
options.queryMode = argv[i];
|
|
96
|
+
if (!['auto', 'forward', 'backward'].includes(options.queryMode)) throw new Error('--query-mode requires auto, forward, or backward');
|
|
97
|
+
} else if (arg === '--query') {
|
|
88
98
|
i += 1;
|
|
89
99
|
if (i >= argv.length) throw new Error('--query requires a value');
|
|
90
100
|
options.query = argv[i];
|
|
@@ -197,15 +207,42 @@
|
|
|
197
207
|
}
|
|
198
208
|
if (fatal) return 1;
|
|
199
209
|
|
|
200
|
-
const result = evaluate(compiled.program, { ...options, analysis: compiled.analysis });
|
|
201
|
-
result.diagnostics = compiled.diagnostics;
|
|
202
|
-
result.analysis = compiled.analysis;
|
|
203
|
-
|
|
204
210
|
const queryText = options.queryFile ? fs.readFileSync(options.queryFile, 'utf8') : options.query;
|
|
205
211
|
const querySpec = queryText
|
|
206
212
|
? parseQuery(queryText, { filename: options.queryFile || '<query>', prefixes: compiled.program.prefixes, baseIRI: compiled.program.baseIRI })
|
|
207
213
|
: null;
|
|
208
|
-
|
|
214
|
+
|
|
215
|
+
let result;
|
|
216
|
+
if (querySpec) {
|
|
217
|
+
const directQuery = queryProgram(compiled.program, querySpec, options);
|
|
218
|
+
if (directQuery) {
|
|
219
|
+
result = {
|
|
220
|
+
baseIRI: compiled.program.baseIRI,
|
|
221
|
+
prefixes: compiled.program.prefixes,
|
|
222
|
+
input: compiled.program.data.slice(),
|
|
223
|
+
inferred: [],
|
|
224
|
+
closure: compiled.program.data.slice(),
|
|
225
|
+
iterations: 0,
|
|
226
|
+
layers: [],
|
|
227
|
+
ruleApplications: 0,
|
|
228
|
+
perRule: [],
|
|
229
|
+
trace: [],
|
|
230
|
+
diagnostics: compiled.diagnostics,
|
|
231
|
+
analysis: compiled.analysis,
|
|
232
|
+
query: directQuery,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!result) {
|
|
238
|
+
const runOptions = querySpec
|
|
239
|
+
? queryRunOptions(compiled.program, querySpec, options)
|
|
240
|
+
: { ...options, hybrid: options.hybrid };
|
|
241
|
+
result = evaluate(compiled.program, { ...runOptions, analysis: compiled.analysis });
|
|
242
|
+
result.diagnostics = compiled.diagnostics;
|
|
243
|
+
result.analysis = compiled.analysis;
|
|
244
|
+
if (querySpec) result.query = queryResult(result, querySpec, runOptions);
|
|
245
|
+
}
|
|
209
246
|
|
|
210
247
|
if (options.json) {
|
|
211
248
|
io.stdout.write(`${JSON.stringify(toJSON(result, { all: options.all, trace: options.trace, analysis: options.deps }), null, 2)}\n`);
|
|
@@ -220,7 +257,9 @@
|
|
|
220
257
|
}
|
|
221
258
|
|
|
222
259
|
if (options.stats) {
|
|
223
|
-
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`);
|
|
260
|
+
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`);
|
|
261
|
+
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`);
|
|
262
|
+
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`);
|
|
224
263
|
for (const rule of result.perRule) {
|
|
225
264
|
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`);
|
|
226
265
|
}
|
|
@@ -246,7 +285,7 @@
|
|
|
246
285
|
const { evaluate } = require('./engine.js');
|
|
247
286
|
const { analyze } = require('./analyze.js');
|
|
248
287
|
const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
|
|
249
|
-
const { runQuery, queryResult } = require('./query.js');
|
|
288
|
+
const { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');
|
|
250
289
|
const { resultTriples } = require('./output.js');
|
|
251
290
|
|
|
252
291
|
function parseInput(source, options = {}) {
|
|
@@ -360,6 +399,9 @@
|
|
|
360
399
|
runToString,
|
|
361
400
|
runQuery,
|
|
362
401
|
queryResult,
|
|
402
|
+
queryProgram,
|
|
403
|
+
queryRunOptions,
|
|
404
|
+
shouldUseHybridForQuery,
|
|
363
405
|
formatTriples,
|
|
364
406
|
formatBindings,
|
|
365
407
|
sortTriples,
|
|
@@ -4278,10 +4320,11 @@
|
|
|
4278
4320
|
"src/engine.js": function (require, module, exports) {
|
|
4279
4321
|
'use strict';
|
|
4280
4322
|
|
|
4281
|
-
const { TripleStore, bindingKey } = require('./store.js');
|
|
4323
|
+
const { TripleStore, bindingKey, instantiateTerm } = require('./store.js');
|
|
4282
4324
|
const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
|
|
4283
4325
|
const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
|
|
4284
4326
|
const { analyze } = require('./analyze.js');
|
|
4327
|
+
const { BackwardProver, preferredBackwardPredicates, ruleIsBackwardOriented } = require('./backward.js');
|
|
4285
4328
|
|
|
4286
4329
|
function evaluate(program, options = {}) {
|
|
4287
4330
|
const maxIterations = options.maxIterations ?? 10000;
|
|
@@ -4297,6 +4340,7 @@
|
|
|
4297
4340
|
applications: 0,
|
|
4298
4341
|
added: 0,
|
|
4299
4342
|
runOnce: !!rule.runOnce,
|
|
4343
|
+
backward: false,
|
|
4300
4344
|
}));
|
|
4301
4345
|
|
|
4302
4346
|
const analysis = options.analysis || analyze(program, options);
|
|
@@ -4313,6 +4357,18 @@
|
|
|
4313
4357
|
const relaxedRecursiveRunOnce = options.relaxedRecursion === false
|
|
4314
4358
|
? new Set()
|
|
4315
4359
|
: recursiveTermGenerationRuleIndexes(analysis);
|
|
4360
|
+
const useHybrid = options.hybrid !== false && !options.shacl12Conformance;
|
|
4361
|
+
const hybridBackwardPredicates = useHybrid || options.backwardBodyCalls
|
|
4362
|
+
? preferredBackwardPredicates(program, options)
|
|
4363
|
+
: new Set();
|
|
4364
|
+
const hybridBackwardRules = new Set();
|
|
4365
|
+
if (hybridBackwardPredicates.size > 0) {
|
|
4366
|
+
for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {
|
|
4367
|
+
if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;
|
|
4371
|
+
for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;
|
|
4316
4372
|
const baseContext = {
|
|
4317
4373
|
...evalOptions,
|
|
4318
4374
|
maxIterations,
|
|
@@ -4324,12 +4380,16 @@
|
|
|
4324
4380
|
iteration: 0,
|
|
4325
4381
|
startingIterations: 0,
|
|
4326
4382
|
recursiveLayer: false,
|
|
4383
|
+
hybridBackwardPredicates,
|
|
4384
|
+
hybridBackwardRules,
|
|
4385
|
+
hybridStats,
|
|
4327
4386
|
};
|
|
4328
4387
|
|
|
4329
4388
|
for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
|
|
4330
4389
|
const layer = layerIndexes[layerIndex];
|
|
4331
|
-
const
|
|
4332
|
-
const
|
|
4390
|
+
const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;
|
|
4391
|
+
const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
|
|
4392
|
+
const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
|
|
4333
4393
|
|
|
4334
4394
|
if (runOnce.length > 0) {
|
|
4335
4395
|
iterations += 1;
|
|
@@ -4362,6 +4422,7 @@
|
|
|
4362
4422
|
ruleApplications,
|
|
4363
4423
|
perRule,
|
|
4364
4424
|
trace,
|
|
4425
|
+
hybridStats,
|
|
4365
4426
|
};
|
|
4366
4427
|
}
|
|
4367
4428
|
|
|
@@ -4431,9 +4492,10 @@
|
|
|
4431
4492
|
const seenBindings = dedupeBindings ? new Set() : null;
|
|
4432
4493
|
const headBlankLabels = collectHeadBlankLabels(rule.head);
|
|
4433
4494
|
|
|
4434
|
-
const
|
|
4495
|
+
const bodyContext = prepareBodyContext(program, store, context);
|
|
4496
|
+
const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)
|
|
4435
4497
|
? store.match(rule.body[0].triple, {})
|
|
4436
|
-
: evaluateBodyStream(rule.body, store, {},
|
|
4498
|
+
: evaluateBodyStream(rule.body, store, {}, bodyContext);
|
|
4437
4499
|
|
|
4438
4500
|
for (const binding of bodyBindings) {
|
|
4439
4501
|
if (seenBindings) {
|
|
@@ -4466,9 +4528,36 @@
|
|
|
4466
4528
|
}
|
|
4467
4529
|
}
|
|
4468
4530
|
|
|
4531
|
+
if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);
|
|
4469
4532
|
return { applications, added };
|
|
4470
4533
|
}
|
|
4471
4534
|
|
|
4535
|
+
function prepareBodyContext(program, store, context) {
|
|
4536
|
+
if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
|
|
4537
|
+
return {
|
|
4538
|
+
...context,
|
|
4539
|
+
backwardProver: new BackwardProver(program, {
|
|
4540
|
+
...context,
|
|
4541
|
+
store,
|
|
4542
|
+
allowedPredicates: context.hybridBackwardPredicates,
|
|
4543
|
+
}),
|
|
4544
|
+
};
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
function emptyBackwardStats() {
|
|
4548
|
+
return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
function mergeBackwardStats(total, item) {
|
|
4552
|
+
if (!total || !item) return;
|
|
4553
|
+
total.goals += item.goals || 0;
|
|
4554
|
+
total.facts += item.facts || 0;
|
|
4555
|
+
total.rules += item.rules || 0;
|
|
4556
|
+
total.memoHits += item.memoHits || 0;
|
|
4557
|
+
total.memoStores += item.memoStores || 0;
|
|
4558
|
+
total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4472
4561
|
function recursiveTermGenerationRuleIndexes(analysis) {
|
|
4473
4562
|
const out = new Set();
|
|
4474
4563
|
if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
|
|
@@ -4577,9 +4666,20 @@
|
|
|
4577
4666
|
|
|
4578
4667
|
const clause = clauses[index];
|
|
4579
4668
|
if (clause.type === 'triple') {
|
|
4669
|
+
const seen = new Set();
|
|
4580
4670
|
for (const matched of store.match(clause.triple, initialBinding)) {
|
|
4671
|
+
const key = bindingKey(matched);
|
|
4672
|
+
seen.add(key);
|
|
4581
4673
|
yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
|
|
4582
4674
|
}
|
|
4675
|
+
if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {
|
|
4676
|
+
for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {
|
|
4677
|
+
const key = bindingKey(matched);
|
|
4678
|
+
if (seen.has(key)) continue;
|
|
4679
|
+
seen.add(key);
|
|
4680
|
+
yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4583
4683
|
return;
|
|
4584
4684
|
}
|
|
4585
4685
|
|
|
@@ -4625,6 +4725,12 @@
|
|
|
4625
4725
|
throw new Error(`Unsupported body clause ${clause.type}`);
|
|
4626
4726
|
}
|
|
4627
4727
|
|
|
4728
|
+
function shouldUseBackwardForTriple(pattern, binding, options = {}) {
|
|
4729
|
+
if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;
|
|
4730
|
+
const predicate = instantiateTerm(pattern.p, binding);
|
|
4731
|
+
return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4628
4734
|
function bodyHasAny(clauses, store, initialBinding, options) {
|
|
4629
4735
|
for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;
|
|
4630
4736
|
return false;
|
|
@@ -4657,6 +4763,7 @@
|
|
|
4657
4763
|
this.byPredicate = new Map();
|
|
4658
4764
|
this.byPredicateSubject = new Map();
|
|
4659
4765
|
this.byPredicateObject = new Map();
|
|
4766
|
+
this.version = 0;
|
|
4660
4767
|
for (const triple of triples) this.add(triple);
|
|
4661
4768
|
}
|
|
4662
4769
|
|
|
@@ -4671,6 +4778,7 @@
|
|
|
4671
4778
|
addIndex(this.byPredicate, predicate, key, normalized);
|
|
4672
4779
|
addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);
|
|
4673
4780
|
addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);
|
|
4781
|
+
this.version += 1;
|
|
4674
4782
|
return true;
|
|
4675
4783
|
}
|
|
4676
4784
|
|
|
@@ -5652,6 +5760,544 @@
|
|
|
5652
5760
|
canPossiblyGenerate,
|
|
5653
5761
|
};
|
|
5654
5762
|
|
|
5763
|
+
},
|
|
5764
|
+
"src/backward.js": function (require, module, exports) {
|
|
5765
|
+
'use strict';
|
|
5766
|
+
|
|
5767
|
+
const { TripleStore, bindingKey } = require('./store.js');
|
|
5768
|
+
const { tripleKey, termKey, termEquals } = require('./term.js');
|
|
5769
|
+
const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
|
|
5770
|
+
|
|
5771
|
+
function backwardQuery(program, querySpec, options = {}) {
|
|
5772
|
+
const planner = planBackwardQuery(program, querySpec, options);
|
|
5773
|
+
if (!planner.ok) return { ok: false, reason: planner.reason };
|
|
5774
|
+
const prover = new BackwardProver(program, { ...options, allowedRuleIndexes: planner.ruleIndexes });
|
|
5775
|
+
const bindings = uniqueBindings(Array.from(prover.solveBody(querySpec.body, {})));
|
|
5776
|
+
return { ok: true, bindings, stats: prover.stats, plan: planner };
|
|
5777
|
+
}
|
|
5778
|
+
|
|
5779
|
+
function planBackwardQuery(program, querySpec, options = {}) {
|
|
5780
|
+
const clauses = querySpec && Array.isArray(querySpec.body) ? querySpec.body : [];
|
|
5781
|
+
if (!bodySupported(clauses, options)) return { ok: false, reason: 'query body contains clauses not supported by the backward prover yet' };
|
|
5782
|
+
|
|
5783
|
+
const reachable = reachableBackwardRuleIndexes(program, clauses, options);
|
|
5784
|
+
for (const ruleIndex of reachable.ruleIndexes) {
|
|
5785
|
+
const rule = (program.rules || [])[ruleIndex];
|
|
5786
|
+
if (!ruleSupported(rule, options)) return { ok: false, reason: `reachable rule ${rule.name || '<anonymous>'} is not supported by the backward prover yet` };
|
|
5787
|
+
}
|
|
5788
|
+
return { ok: true, ruleIndexes: reachable.ruleIndexes, predicates: reachable.predicates };
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
function bodySupported(clauses, options = {}) {
|
|
5792
|
+
for (const clause of clauses || []) {
|
|
5793
|
+
if (clause.type === 'triple' || clause.type === 'filter' || clause.type === 'set' || clause.type === 'bind') continue;
|
|
5794
|
+
if (clause.type === 'not') {
|
|
5795
|
+
if (options.backwardNegation === false) return false;
|
|
5796
|
+
if (!bodySupported(clause.body, options)) return false;
|
|
5797
|
+
continue;
|
|
5798
|
+
}
|
|
5799
|
+
return false;
|
|
5800
|
+
}
|
|
5801
|
+
return true;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
function ruleSupported(rule, options = {}) {
|
|
5805
|
+
if (!Array.isArray(rule.head) || rule.head.length === 0) return false;
|
|
5806
|
+
for (const head of rule.head) {
|
|
5807
|
+
if (!head || !head.p || head.p.type !== 'iri') return false;
|
|
5808
|
+
if (containsBlank(head.s) || containsBlank(head.p) || containsBlank(head.o)) return false;
|
|
5809
|
+
}
|
|
5810
|
+
return bodySupported(rule.body || [], options);
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
class BackwardProver {
|
|
5814
|
+
constructor(program, options = {}) {
|
|
5815
|
+
this.program = program;
|
|
5816
|
+
this.options = options;
|
|
5817
|
+
this.store = options.store || new TripleStore(program.data || []);
|
|
5818
|
+
this.maxDepth = options.backwardMaxDepth || options.maxDepth || 10000;
|
|
5819
|
+
this.solutionLimit = options.backwardSolutionLimit || options.solutionLimit || 1000000;
|
|
5820
|
+
this.allowedPredicates = normalizePredicateSet(options.allowedPredicates || options.backwardPredicates || null);
|
|
5821
|
+
this.allowedRuleIndexes = normalizeRuleIndexSet(options.allowedRuleIndexes || null);
|
|
5822
|
+
this.ruleHeads = indexRuleHeads(program.rules || [], { allowedPredicates: this.allowedPredicates, allowedRuleIndexes: this.allowedRuleIndexes });
|
|
5823
|
+
this.memo = new Map();
|
|
5824
|
+
this.active = new Set();
|
|
5825
|
+
this.freshCounter = 0;
|
|
5826
|
+
this.solutionCount = 0;
|
|
5827
|
+
this.stats = {
|
|
5828
|
+
mode: 'backward',
|
|
5829
|
+
goals: 0,
|
|
5830
|
+
facts: 0,
|
|
5831
|
+
rules: 0,
|
|
5832
|
+
memoHits: 0,
|
|
5833
|
+
memoStores: 0,
|
|
5834
|
+
maxDepth: 0,
|
|
5835
|
+
};
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5838
|
+
*solveBody(clauses, binding = {}, depth = 0, index = 0) {
|
|
5839
|
+
if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);
|
|
5840
|
+
this.stats.maxDepth = Math.max(this.stats.maxDepth, depth);
|
|
5841
|
+
if (this.solutionCount >= this.solutionLimit) return;
|
|
5842
|
+
if (index >= clauses.length) {
|
|
5843
|
+
this.solutionCount += 1;
|
|
5844
|
+
yield resolveBinding(binding);
|
|
5845
|
+
return;
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5848
|
+
const clause = clauses[index];
|
|
5849
|
+
if (clause.type === 'triple') {
|
|
5850
|
+
for (const matched of this.solveTriple(clause.triple, binding, depth + 1)) {
|
|
5851
|
+
yield* this.solveBody(clauses, matched, depth + 1, index + 1);
|
|
5852
|
+
}
|
|
5853
|
+
return;
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
if (clause.type === 'filter') {
|
|
5857
|
+
try {
|
|
5858
|
+
if (booleanValue(evalExpression(clause.expr, resolveBinding(binding), this.options))) {
|
|
5859
|
+
yield* this.solveBody(clauses, binding, depth + 1, index + 1);
|
|
5860
|
+
}
|
|
5861
|
+
} catch (_) {
|
|
5862
|
+
// SPARQL-style FILTER errors reject the current solution.
|
|
5863
|
+
}
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
if (clause.type === 'set' || clause.type === 'bind') {
|
|
5868
|
+
try {
|
|
5869
|
+
const resolved = resolveBinding(binding);
|
|
5870
|
+
const value = asTerm(evalExpression(clause.expr, resolved, this.options));
|
|
5871
|
+
const next = unifyTerms({ type: 'var', value: clause.variable }, value, binding);
|
|
5872
|
+
if (next) yield* this.solveBody(clauses, next, depth + 1, index + 1);
|
|
5873
|
+
} catch (_) {
|
|
5874
|
+
// Assignment errors drop the current solution.
|
|
5875
|
+
}
|
|
5876
|
+
return;
|
|
5877
|
+
}
|
|
5878
|
+
|
|
5879
|
+
if (clause.type === 'not') {
|
|
5880
|
+
let found = false;
|
|
5881
|
+
for (const _ of this.solveBody(clause.body, { ...binding }, depth + 1, 0)) { found = true; break; }
|
|
5882
|
+
if (!found) yield* this.solveBody(clauses, binding, depth + 1, index + 1);
|
|
5883
|
+
return;
|
|
5884
|
+
}
|
|
5885
|
+
|
|
5886
|
+
throw new Error(`Unsupported backward body clause ${clause.type}`);
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
*solveTriple(pattern, binding = {}, depth = 0) {
|
|
5890
|
+
if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);
|
|
5891
|
+
this.stats.goals += 1;
|
|
5892
|
+
const resolvedPattern = resolvePattern(pattern, binding);
|
|
5893
|
+
const key = `${goalKey(resolvedPattern)}@store:${this.store.version || 0}`;
|
|
5894
|
+
const entry = this.memo.get(key);
|
|
5895
|
+
if (entry && entry.complete) {
|
|
5896
|
+
this.stats.memoHits += 1;
|
|
5897
|
+
yield* this.replayAnswers(pattern, binding, entry.answers);
|
|
5898
|
+
return;
|
|
5899
|
+
}
|
|
5900
|
+
if (this.active.has(key)) return;
|
|
5901
|
+
|
|
5902
|
+
const answers = [];
|
|
5903
|
+
const answerKeys = new Set();
|
|
5904
|
+
this.active.add(key);
|
|
5905
|
+
try {
|
|
5906
|
+
for (const fact of this.factCandidates(resolvedPattern, binding)) {
|
|
5907
|
+
const next = unifyTriples(pattern, fact, binding);
|
|
5908
|
+
if (!next) continue;
|
|
5909
|
+
rememberAnswer(answers, answerKeys, pattern, next);
|
|
5910
|
+
this.stats.facts += 1;
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
for (const item of this.ruleCandidates(resolvedPattern)) {
|
|
5914
|
+
const suffix = `__b${++this.freshCounter}_${item.ruleIndex}`;
|
|
5915
|
+
const freshHead = freshTriple(item.head, suffix);
|
|
5916
|
+
const next = unifyTriples(pattern, freshHead, binding);
|
|
5917
|
+
if (!next) continue;
|
|
5918
|
+
const freshBody = (item.rule.body || []).map((clause) => freshClause(clause, suffix));
|
|
5919
|
+
for (const solved of this.solveBody(freshBody, next, depth + 1, 0)) {
|
|
5920
|
+
rememberAnswer(answers, answerKeys, pattern, solved);
|
|
5921
|
+
this.stats.rules += 1;
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
} finally {
|
|
5925
|
+
this.active.delete(key);
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
this.memo.set(key, { complete: true, answers });
|
|
5929
|
+
this.stats.memoStores += 1;
|
|
5930
|
+
yield* this.replayAnswers(pattern, binding, answers);
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
*replayAnswers(pattern, binding, answers) {
|
|
5934
|
+
for (const answer of answers) {
|
|
5935
|
+
const next = unifyTriples(pattern, answer, binding);
|
|
5936
|
+
if (next) yield next;
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
|
|
5940
|
+
factCandidates(pattern, binding) {
|
|
5941
|
+
return this.store.candidates(pattern, binding);
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
ruleCandidates(pattern) {
|
|
5945
|
+
const predicate = pattern.p && pattern.p.type === 'iri' ? pattern.p.value : null;
|
|
5946
|
+
if (predicate) return this.ruleHeads.byPredicate.get(predicate) || [];
|
|
5947
|
+
return this.ruleHeads.all;
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
|
|
5951
|
+
function indexRuleHeads(rules, options = {}) {
|
|
5952
|
+
const allowedPredicates = options.allowedPredicates || null;
|
|
5953
|
+
const allowedRuleIndexes = options.allowedRuleIndexes || null;
|
|
5954
|
+
const byPredicate = new Map();
|
|
5955
|
+
const all = [];
|
|
5956
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
5957
|
+
if (allowedRuleIndexes && !allowedRuleIndexes.has(ruleIndex)) continue;
|
|
5958
|
+
const rule = rules[ruleIndex];
|
|
5959
|
+
for (let headIndex = 0; headIndex < (rule.head || []).length; headIndex += 1) {
|
|
5960
|
+
const head = rule.head[headIndex];
|
|
5961
|
+
if (!head || !head.p || head.p.type !== 'iri') continue;
|
|
5962
|
+
if (allowedPredicates && !allowedPredicates.has(head.p.value)) continue;
|
|
5963
|
+
const item = { ruleIndex, headIndex, rule, head };
|
|
5964
|
+
all.push(item);
|
|
5965
|
+
const bucket = byPredicate.get(head.p.value);
|
|
5966
|
+
if (bucket) bucket.push(item);
|
|
5967
|
+
else byPredicate.set(head.p.value, [item]);
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
return { byPredicate, all };
|
|
5971
|
+
}
|
|
5972
|
+
|
|
5973
|
+
function freshClause(clause, suffix) {
|
|
5974
|
+
if (clause.type === 'triple') return { ...clause, triple: freshTriple(clause.triple, suffix) };
|
|
5975
|
+
if (clause.type === 'filter') return { ...clause, expr: freshExpr(clause.expr, suffix) };
|
|
5976
|
+
if (clause.type === 'set' || clause.type === 'bind') return { ...clause, variable: freshVarName(clause.variable, suffix), expr: freshExpr(clause.expr, suffix) };
|
|
5977
|
+
if (clause.type === 'not') return { ...clause, body: clause.body.map((item) => freshClause(item, suffix)) };
|
|
5978
|
+
return clause;
|
|
5979
|
+
}
|
|
5980
|
+
|
|
5981
|
+
function freshTriple(triple, suffix) {
|
|
5982
|
+
return { s: freshTerm(triple.s, suffix), p: freshTerm(triple.p, suffix), o: freshTerm(triple.o, suffix) };
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
function freshTerm(term, suffix) {
|
|
5986
|
+
if (!term) return term;
|
|
5987
|
+
if (term.type === 'var') return { type: 'var', value: freshVarName(term.value, suffix) };
|
|
5988
|
+
if (term.type === 'triple') return { type: 'triple', s: freshTerm(term.s, suffix), p: freshTerm(term.p, suffix), o: freshTerm(term.o, suffix) };
|
|
5989
|
+
return term;
|
|
5990
|
+
}
|
|
5991
|
+
|
|
5992
|
+
function freshExpr(expr, suffix) {
|
|
5993
|
+
if (!expr) return expr;
|
|
5994
|
+
if (expr.type === 'var') return { ...expr, name: freshVarName(expr.name, suffix) };
|
|
5995
|
+
if (expr.type === 'term') return { ...expr, value: freshTerm(expr.value, suffix) };
|
|
5996
|
+
if (expr.type === 'unary') return { ...expr, expr: freshExpr(expr.expr, suffix) };
|
|
5997
|
+
if (expr.type === 'binary') return { ...expr, left: freshExpr(expr.left, suffix), right: freshExpr(expr.right, suffix) };
|
|
5998
|
+
if (expr.type === 'call') return { ...expr, args: expr.args.map((arg) => freshExpr(arg, suffix)) };
|
|
5999
|
+
if (expr.type === 'list') return { ...expr, items: expr.items.map((item) => freshExpr(item, suffix)) };
|
|
6000
|
+
return expr;
|
|
6001
|
+
}
|
|
6002
|
+
|
|
6003
|
+
function freshVarName(name, suffix) {
|
|
6004
|
+
return `${name}${suffix}`;
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
function resolvePattern(pattern, binding) {
|
|
6008
|
+
return { s: resolveTerm(pattern.s, binding, false), p: resolveTerm(pattern.p, binding, false), o: resolveTerm(pattern.o, binding, false) };
|
|
6009
|
+
}
|
|
6010
|
+
|
|
6011
|
+
function resolveBinding(binding) {
|
|
6012
|
+
const out = {};
|
|
6013
|
+
for (const name of Object.keys(binding)) out[name] = resolveTerm(binding[name], binding, false);
|
|
6014
|
+
return out;
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
function resolveTerm(term, binding, preserveUnbound = true, seen = new Set()) {
|
|
6018
|
+
if (!term) return term;
|
|
6019
|
+
if (term.type === 'var') {
|
|
6020
|
+
const name = term.value;
|
|
6021
|
+
if (seen.has(name)) return preserveUnbound ? term : { type: 'var', value: name };
|
|
6022
|
+
if (!Object.prototype.hasOwnProperty.call(binding, name)) return preserveUnbound ? term : { type: 'var', value: name };
|
|
6023
|
+
seen.add(name);
|
|
6024
|
+
return resolveTerm(binding[name], binding, preserveUnbound, seen);
|
|
6025
|
+
}
|
|
6026
|
+
if (term.type === 'triple') {
|
|
6027
|
+
return {
|
|
6028
|
+
type: 'triple',
|
|
6029
|
+
s: resolveTerm(term.s, binding, preserveUnbound, new Set(seen)),
|
|
6030
|
+
p: resolveTerm(term.p, binding, preserveUnbound, new Set(seen)),
|
|
6031
|
+
o: resolveTerm(term.o, binding, preserveUnbound, new Set(seen)),
|
|
6032
|
+
};
|
|
6033
|
+
}
|
|
6034
|
+
return term;
|
|
6035
|
+
}
|
|
6036
|
+
|
|
6037
|
+
function unifyTriples(left, right, binding) {
|
|
6038
|
+
let next = unifyTerms(left.s, right.s, binding);
|
|
6039
|
+
if (!next) return null;
|
|
6040
|
+
next = unifyTerms(left.p, right.p, next);
|
|
6041
|
+
if (!next) return null;
|
|
6042
|
+
return unifyTerms(left.o, right.o, next);
|
|
6043
|
+
}
|
|
6044
|
+
|
|
6045
|
+
function unifyTerms(left, right, binding) {
|
|
6046
|
+
const a = resolveTerm(left, binding);
|
|
6047
|
+
const b = resolveTerm(right, binding);
|
|
6048
|
+
if (a.type === 'var' && b.type === 'var' && a.value === b.value) return binding;
|
|
6049
|
+
if (a.type === 'var') return bindVariable(a.value, b, binding);
|
|
6050
|
+
if (b.type === 'var') return bindVariable(b.value, a, binding);
|
|
6051
|
+
if (a.type === 'triple' || b.type === 'triple') {
|
|
6052
|
+
if (a.type !== 'triple' || b.type !== 'triple') return null;
|
|
6053
|
+
let next = unifyTerms(a.s, b.s, binding);
|
|
6054
|
+
if (!next) return null;
|
|
6055
|
+
next = unifyTerms(a.p, b.p, next);
|
|
6056
|
+
if (!next) return null;
|
|
6057
|
+
return unifyTerms(a.o, b.o, next);
|
|
6058
|
+
}
|
|
6059
|
+
return termEquals(a, b) ? binding : null;
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
function bindVariable(name, term, binding) {
|
|
6063
|
+
const existing = binding[name];
|
|
6064
|
+
if (existing) return unifyTerms(existing, term, binding);
|
|
6065
|
+
if (term.type === 'var' && term.value === name) return binding;
|
|
6066
|
+
return { ...binding, [name]: term };
|
|
6067
|
+
}
|
|
6068
|
+
|
|
6069
|
+
function rememberAnswer(answers, answerKeys, pattern, binding) {
|
|
6070
|
+
const triple = {
|
|
6071
|
+
s: resolveTerm(pattern.s, binding),
|
|
6072
|
+
p: resolveTerm(pattern.p, binding),
|
|
6073
|
+
o: resolveTerm(pattern.o, binding),
|
|
6074
|
+
};
|
|
6075
|
+
if (triple.s.type === 'var' || triple.p.type === 'var' || triple.o.type === 'var') return;
|
|
6076
|
+
const key = tripleKey(triple);
|
|
6077
|
+
if (answerKeys.has(key)) return;
|
|
6078
|
+
answerKeys.add(key);
|
|
6079
|
+
answers.push(triple);
|
|
6080
|
+
}
|
|
6081
|
+
|
|
6082
|
+
function goalKey(pattern) {
|
|
6083
|
+
return `${safeTermKey(pattern.s)} ${safeTermKey(pattern.p)} ${safeTermKey(pattern.o)}`;
|
|
6084
|
+
}
|
|
6085
|
+
|
|
6086
|
+
function safeTermKey(term) {
|
|
6087
|
+
return term && term.type === 'var' ? '_' : termKey(term);
|
|
6088
|
+
}
|
|
6089
|
+
|
|
6090
|
+
function uniqueBindings(bindings) {
|
|
6091
|
+
const seen = new Set();
|
|
6092
|
+
const out = [];
|
|
6093
|
+
for (const binding of bindings) {
|
|
6094
|
+
const resolved = resolveBinding(binding);
|
|
6095
|
+
const key = bindingKey(resolved);
|
|
6096
|
+
if (seen.has(key)) continue;
|
|
6097
|
+
seen.add(key);
|
|
6098
|
+
out.push(resolved);
|
|
6099
|
+
}
|
|
6100
|
+
return out;
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
function containsBlank(term) {
|
|
6104
|
+
if (!term) return false;
|
|
6105
|
+
if (term.type === 'blank') return true;
|
|
6106
|
+
if (term.type === 'triple') return containsBlank(term.s) || containsBlank(term.p) || containsBlank(term.o);
|
|
6107
|
+
return false;
|
|
6108
|
+
}
|
|
6109
|
+
|
|
6110
|
+
|
|
6111
|
+
|
|
6112
|
+
function reachableBackwardRuleIndexes(program, rootClauses, options = {}) {
|
|
6113
|
+
const rules = program.rules || [];
|
|
6114
|
+
const headIndex = new Map();
|
|
6115
|
+
const allHeadPredicates = new Set();
|
|
6116
|
+
const allRuleIndexes = new Set();
|
|
6117
|
+
for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
|
|
6118
|
+
const rule = rules[ruleIndex];
|
|
6119
|
+
for (const head of rule.head || []) {
|
|
6120
|
+
if (!head || !head.p || head.p.type !== 'iri') continue;
|
|
6121
|
+
allRuleIndexes.add(ruleIndex);
|
|
6122
|
+
allHeadPredicates.add(head.p.value);
|
|
6123
|
+
if (!headIndex.has(head.p.value)) headIndex.set(head.p.value, new Set());
|
|
6124
|
+
headIndex.get(head.p.value).add(ruleIndex);
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
|
|
6128
|
+
const predicates = new Set();
|
|
6129
|
+
const ruleIndexes = new Set();
|
|
6130
|
+
const work = [];
|
|
6131
|
+
const enqueue = (predicate) => {
|
|
6132
|
+
if (!predicate) {
|
|
6133
|
+
for (const item of allHeadPredicates) enqueue(item);
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
if (predicates.has(predicate)) return;
|
|
6137
|
+
predicates.add(predicate);
|
|
6138
|
+
work.push(predicate);
|
|
6139
|
+
};
|
|
6140
|
+
|
|
6141
|
+
for (const predicate of bodyPredicateDemands(rootClauses || [])) enqueue(predicate);
|
|
6142
|
+
|
|
6143
|
+
while (work.length > 0) {
|
|
6144
|
+
const predicate = work.shift();
|
|
6145
|
+
const indexes = headIndex.get(predicate);
|
|
6146
|
+
if (!indexes) continue;
|
|
6147
|
+
for (const ruleIndex of indexes) {
|
|
6148
|
+
if (ruleIndexes.has(ruleIndex)) continue;
|
|
6149
|
+
ruleIndexes.add(ruleIndex);
|
|
6150
|
+
const rule = rules[ruleIndex];
|
|
6151
|
+
for (const needed of bodyPredicateDemands(rule.body || [])) enqueue(needed);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
return { ruleIndexes, predicates };
|
|
6156
|
+
}
|
|
6157
|
+
|
|
6158
|
+
function bodyPredicateDemands(clauses) {
|
|
6159
|
+
const out = [];
|
|
6160
|
+
for (const clause of clauses || []) {
|
|
6161
|
+
if (clause.type === 'triple') {
|
|
6162
|
+
out.push(predicateDemand(clause.triple.p));
|
|
6163
|
+
continue;
|
|
6164
|
+
}
|
|
6165
|
+
if (clause.type === 'not') {
|
|
6166
|
+
out.push(...bodyPredicateDemands(clause.body || []));
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
return out;
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
function predicateDemand(term) {
|
|
6173
|
+
return term && term.type === 'iri' ? term.value : null;
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
function normalizePredicateSet(value) {
|
|
6177
|
+
if (!value) return null;
|
|
6178
|
+
if (value instanceof Set) return value;
|
|
6179
|
+
if (Array.isArray(value)) return new Set(value);
|
|
6180
|
+
return new Set(String(value).split(',').map((item) => item.trim()).filter(Boolean));
|
|
6181
|
+
}
|
|
6182
|
+
|
|
6183
|
+
function normalizeRuleIndexSet(value) {
|
|
6184
|
+
if (!value) return null;
|
|
6185
|
+
if (value instanceof Set) return value;
|
|
6186
|
+
if (Array.isArray(value)) return new Set(value);
|
|
6187
|
+
return null;
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
function supportedBackwardPredicates(program, options = {}) {
|
|
6191
|
+
const explicit = normalizePredicateSet(options.backwardPredicates || options.hybridPredicates || null);
|
|
6192
|
+
const byPredicate = new Map();
|
|
6193
|
+
for (const rule of program.rules || []) {
|
|
6194
|
+
const predicates = new Set();
|
|
6195
|
+
for (const head of rule.head || []) {
|
|
6196
|
+
if (head && head.p && head.p.type === 'iri') predicates.add(head.p.value);
|
|
6197
|
+
}
|
|
6198
|
+
for (const predicate of predicates) {
|
|
6199
|
+
if (!byPredicate.has(predicate)) byPredicate.set(predicate, []);
|
|
6200
|
+
byPredicate.get(predicate).push(rule);
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
const supported = new Set();
|
|
6205
|
+
for (const [predicate, rules] of byPredicate) {
|
|
6206
|
+
if (explicit && !explicit.has(predicate)) continue;
|
|
6207
|
+
if (rules.length > 0 && rules.every((rule) => ruleSupported(rule, options))) supported.add(predicate);
|
|
6208
|
+
}
|
|
6209
|
+
return supported;
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
function preferredBackwardPredicates(program, options = {}) {
|
|
6213
|
+
const explicit = normalizePredicateSet(options.hybridPredicates || null);
|
|
6214
|
+
if (explicit) return supportedBackwardPredicates(program, { ...options, hybridPredicates: explicit });
|
|
6215
|
+
const supported = supportedBackwardPredicates(program, options);
|
|
6216
|
+
const preferred = new Set();
|
|
6217
|
+
const force = options.hybrid === true || options.hybridMode === 'force';
|
|
6218
|
+
const demanded = force ? null : demandedBodyPredicates(program);
|
|
6219
|
+
for (const rule of program.rules || []) {
|
|
6220
|
+
if (!ruleIsFunctionLike(rule)) continue;
|
|
6221
|
+
if (!force && ruleCreatesHeadTerms(rule)) continue;
|
|
6222
|
+
for (const head of rule.head || []) {
|
|
6223
|
+
if (!head || !head.p || head.p.type !== 'iri' || !supported.has(head.p.value)) continue;
|
|
6224
|
+
if (force || demanded.has(head.p.value)) preferred.add(head.p.value);
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
return preferred;
|
|
6228
|
+
}
|
|
6229
|
+
|
|
6230
|
+
function demandedBodyPredicates(program) {
|
|
6231
|
+
const out = new Set();
|
|
6232
|
+
for (const rule of program.rules || []) {
|
|
6233
|
+
for (const predicate of bodyPredicateDemands(rule.body || [])) if (predicate) out.add(predicate);
|
|
6234
|
+
}
|
|
6235
|
+
return out;
|
|
6236
|
+
}
|
|
6237
|
+
|
|
6238
|
+
|
|
6239
|
+
function ruleCreatesHeadTerms(rule) {
|
|
6240
|
+
const headVars = new Set();
|
|
6241
|
+
for (const triple of rule.head || []) {
|
|
6242
|
+
for (const term of [triple.s, triple.p, triple.o]) {
|
|
6243
|
+
if (!term) continue;
|
|
6244
|
+
if (term.type === 'blank') return true;
|
|
6245
|
+
if (term.type === 'var') headVars.add(term.value);
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
if (headVars.size === 0) return false;
|
|
6249
|
+
for (const clause of rule.body || []) {
|
|
6250
|
+
if ((clause.type === 'set' || clause.type === 'bind') && headVars.has(clause.variable) && expressionCreatesTerm(clause.expr)) return true;
|
|
6251
|
+
}
|
|
6252
|
+
return false;
|
|
6253
|
+
}
|
|
6254
|
+
|
|
6255
|
+
function expressionCreatesTerm(expr) {
|
|
6256
|
+
if (!expr) return false;
|
|
6257
|
+
if (expr.type === 'call') {
|
|
6258
|
+
const name = String(expr.name || '').toUpperCase();
|
|
6259
|
+
if (name === 'BNODE' || name === 'IRI' || name === 'URI' || name === 'TRIPLE' || name === 'UUID' || name === 'STRUUID') return true;
|
|
6260
|
+
return (expr.args || []).some(expressionCreatesTerm);
|
|
6261
|
+
}
|
|
6262
|
+
if (expr.type === 'binary') return expressionCreatesTerm(expr.left) || expressionCreatesTerm(expr.right);
|
|
6263
|
+
if (expr.type === 'unary') return expressionCreatesTerm(expr.expr);
|
|
6264
|
+
if (expr.type === 'in') return expressionCreatesTerm(expr.left) || (expr.values || []).some(expressionCreatesTerm);
|
|
6265
|
+
return false;
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
function ruleIsFunctionLike(rule) {
|
|
6269
|
+
return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
|
|
6270
|
+
}
|
|
6271
|
+
|
|
6272
|
+
function ruleHeadPredicates(rule) {
|
|
6273
|
+
const predicates = new Set();
|
|
6274
|
+
for (const head of rule.head || []) {
|
|
6275
|
+
if (!head || !head.p || head.p.type !== 'iri') return null;
|
|
6276
|
+
predicates.add(head.p.value);
|
|
6277
|
+
}
|
|
6278
|
+
return predicates;
|
|
6279
|
+
}
|
|
6280
|
+
|
|
6281
|
+
function ruleIsBackwardOriented(rule, predicates) {
|
|
6282
|
+
if (!predicates || predicates.size === 0) return false;
|
|
6283
|
+
const heads = ruleHeadPredicates(rule);
|
|
6284
|
+
if (!heads || heads.size === 0) return false;
|
|
6285
|
+
for (const predicate of heads) if (!predicates.has(predicate)) return false;
|
|
6286
|
+
return true;
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
module.exports = {
|
|
6290
|
+
BackwardProver,
|
|
6291
|
+
backwardQuery,
|
|
6292
|
+
planBackwardQuery,
|
|
6293
|
+
supportedBackwardPredicates,
|
|
6294
|
+
preferredBackwardPredicates,
|
|
6295
|
+
reachableBackwardRuleIndexes,
|
|
6296
|
+
ruleIsBackwardOriented,
|
|
6297
|
+
ruleSupported,
|
|
6298
|
+
resolveBinding,
|
|
6299
|
+
};
|
|
6300
|
+
|
|
5655
6301
|
},
|
|
5656
6302
|
"src/format.js": function (require, module, exports) {
|
|
5657
6303
|
'use strict';
|
|
@@ -5745,6 +6391,7 @@
|
|
|
5745
6391
|
const { parseQuery } = require('./parser.js');
|
|
5746
6392
|
const { TripleStore, bindingKey } = require('./store.js');
|
|
5747
6393
|
const { evaluateBody } = require('./engine.js');
|
|
6394
|
+
const { backwardQuery, planBackwardQuery, preferredBackwardPredicates } = require('./backward.js');
|
|
5748
6395
|
|
|
5749
6396
|
function queryResult(result, querySpec, options = {}) {
|
|
5750
6397
|
const store = new TripleStore(result.closure || []);
|
|
@@ -5755,28 +6402,92 @@
|
|
|
5755
6402
|
prefixes: result.prefixes,
|
|
5756
6403
|
select,
|
|
5757
6404
|
bindings: projectBindings(bindings, select),
|
|
6405
|
+
mode: result.hybridStats ? 'hybrid' : 'forward',
|
|
5758
6406
|
};
|
|
5759
6407
|
}
|
|
5760
6408
|
|
|
6409
|
+
function queryProgram(program, querySpec, options = {}) {
|
|
6410
|
+
const mode = options.queryMode || 'auto';
|
|
6411
|
+
if (mode !== 'forward' && mode !== 'hybrid') {
|
|
6412
|
+
const planned = planBackwardQuery(program, querySpec, options);
|
|
6413
|
+
if (planned.ok) {
|
|
6414
|
+
const result = backwardQuery(program, querySpec, options);
|
|
6415
|
+
if (result.ok) {
|
|
6416
|
+
const select = normalizeSelect(querySpec.select, result.bindings);
|
|
6417
|
+
return {
|
|
6418
|
+
baseIRI: program.baseIRI,
|
|
6419
|
+
prefixes: program.prefixes,
|
|
6420
|
+
select,
|
|
6421
|
+
bindings: projectBindings(result.bindings, select),
|
|
6422
|
+
mode: 'backward',
|
|
6423
|
+
stats: result.stats,
|
|
6424
|
+
};
|
|
6425
|
+
}
|
|
6426
|
+
if (mode === 'backward') throw new Error(result.reason || 'Backward query failed');
|
|
6427
|
+
} else if (mode === 'backward') {
|
|
6428
|
+
throw new Error(`Backward query is not supported for this ruleset: ${planned.reason}`);
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
return null;
|
|
6432
|
+
}
|
|
6433
|
+
|
|
5761
6434
|
function runQuery(source, querySource = null, options = {}) {
|
|
5762
6435
|
const { run, compile } = require('./api.js');
|
|
5763
|
-
const { program, diagnostics } = compile(source, options);
|
|
5764
|
-
const result = run(program, options);
|
|
5765
|
-
result.diagnostics = diagnostics;
|
|
6436
|
+
const { program, diagnostics, analysis } = compile(source, options);
|
|
5766
6437
|
|
|
5767
6438
|
let querySpec;
|
|
5768
6439
|
if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });
|
|
5769
6440
|
else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');
|
|
5770
6441
|
|
|
5771
|
-
const
|
|
5772
|
-
|
|
6442
|
+
const direct = queryProgram(program, querySpec, options);
|
|
6443
|
+
if (direct) {
|
|
6444
|
+
return {
|
|
6445
|
+
baseIRI: program.baseIRI,
|
|
6446
|
+
version: program.version || null,
|
|
6447
|
+
imports: program.imports || [],
|
|
6448
|
+
prefixes: program.prefixes,
|
|
6449
|
+
input: program.data.slice(),
|
|
6450
|
+
inferred: [],
|
|
6451
|
+
closure: program.data.slice(),
|
|
6452
|
+
iterations: 0,
|
|
6453
|
+
layers: [],
|
|
6454
|
+
ruleApplications: 0,
|
|
6455
|
+
perRule: [],
|
|
6456
|
+
trace: [],
|
|
6457
|
+
diagnostics,
|
|
6458
|
+
analysis,
|
|
6459
|
+
query: direct,
|
|
6460
|
+
};
|
|
6461
|
+
}
|
|
6462
|
+
|
|
6463
|
+
const runOptions = queryRunOptions(program, querySpec, options);
|
|
6464
|
+
const result = run(program, runOptions);
|
|
6465
|
+
result.diagnostics = diagnostics;
|
|
6466
|
+
result.query = queryResult(result, querySpec, runOptions);
|
|
6467
|
+
return result;
|
|
6468
|
+
}
|
|
6469
|
+
|
|
6470
|
+
function queryRunOptions(program, querySpec, options = {}) {
|
|
6471
|
+
const mode = options.queryMode || 'auto';
|
|
6472
|
+
if (mode === 'forward') return { ...options, hybrid: false };
|
|
6473
|
+
if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: options.hybrid ?? 'auto' };
|
|
6474
|
+
return options;
|
|
6475
|
+
}
|
|
6476
|
+
|
|
6477
|
+
function shouldUseHybridForQuery(program, querySpec, options = {}) {
|
|
6478
|
+
const mode = options.queryMode || 'auto';
|
|
6479
|
+
if (options.hybrid === false) return false;
|
|
6480
|
+
if (options.hybrid === true) return true;
|
|
6481
|
+
if (mode !== 'auto') return false;
|
|
6482
|
+
if (!querySpec) return false;
|
|
6483
|
+
return preferredBackwardPredicates(program, options).size > 0;
|
|
5773
6484
|
}
|
|
5774
6485
|
|
|
5775
6486
|
function normalizeSelect(select, bindings) {
|
|
5776
6487
|
if (select && select.length > 0) return select.slice();
|
|
5777
6488
|
const vars = new Set();
|
|
5778
6489
|
for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);
|
|
5779
|
-
return Array.from(vars).sort();
|
|
6490
|
+
return Array.from(vars).sort().filter((name) => !name.includes('__b'));
|
|
5780
6491
|
}
|
|
5781
6492
|
|
|
5782
6493
|
function projectBindings(bindings, select) {
|
|
@@ -5794,7 +6505,7 @@
|
|
|
5794
6505
|
return out;
|
|
5795
6506
|
}
|
|
5796
6507
|
|
|
5797
|
-
module.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };
|
|
6508
|
+
module.exports = { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery, parseQuery, normalizeSelect, projectBindings };
|
|
5798
6509
|
|
|
5799
6510
|
},
|
|
5800
6511
|
"src/output.js": function (require, module, exports) {
|
|
@@ -5808,7 +6519,7 @@
|
|
|
5808
6519
|
|
|
5809
6520
|
},
|
|
5810
6521
|
};
|
|
5811
|
-
const __mappings = {"src/tokenizer.js":{},"src/assignments.js":{},"src/term.js":{},"src/rdfSyntax.js":{"./tokenizer.js":"src/tokenizer.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/builtins.js":{"./term.js":"src/term.js"},"src/parser.js":{"./tokenizer.js":"src/tokenizer.js","./rdfSyntax.js":"src/rdfSyntax.js","./builtins.js":"src/builtins.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/rdfMessages.js":{"./rdfSyntax.js":"src/rdfSyntax.js","./term.js":"src/term.js"},"src/store.js":{"./term.js":"src/term.js"},"src/analyze.js":{"./term.js":"src/term.js","./assignments.js":"src/assignments.js"},"src/engine.js":{"./store.js":"src/store.js","./term.js":"src/term.js","./builtins.js":"src/builtins.js","./analyze.js":"src/analyze.js"},"src/format.js":{"./term.js":"src/term.js"},"src/query.js":{"./parser.js":"src/parser.js","./store.js":"src/store.js","./engine.js":"src/engine.js","./api.js":"src/api.js"},"src/output.js":{},"src/api.js":{"./parser.js":"src/parser.js","./rdfSyntax.js":"src/rdfSyntax.js","./rdfMessages.js":"src/rdfMessages.js","./engine.js":"src/engine.js","./analyze.js":"src/analyze.js","./format.js":"src/format.js","./query.js":"src/query.js","./output.js":"src/output.js"},"src/cli.js":{"./api.js":"src/api.js","./term.js":"src/term.js"}};
|
|
6522
|
+
const __mappings = {"src/tokenizer.js":{},"src/assignments.js":{},"src/term.js":{},"src/rdfSyntax.js":{"./tokenizer.js":"src/tokenizer.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/builtins.js":{"./term.js":"src/term.js"},"src/parser.js":{"./tokenizer.js":"src/tokenizer.js","./rdfSyntax.js":"src/rdfSyntax.js","./builtins.js":"src/builtins.js","./assignments.js":"src/assignments.js","./term.js":"src/term.js"},"src/rdfMessages.js":{"./rdfSyntax.js":"src/rdfSyntax.js","./term.js":"src/term.js"},"src/store.js":{"./term.js":"src/term.js"},"src/analyze.js":{"./term.js":"src/term.js","./assignments.js":"src/assignments.js"},"src/backward.js":{"./store.js":"src/store.js","./term.js":"src/term.js","./builtins.js":"src/builtins.js"},"src/engine.js":{"./store.js":"src/store.js","./term.js":"src/term.js","./builtins.js":"src/builtins.js","./analyze.js":"src/analyze.js","./backward.js":"src/backward.js"},"src/format.js":{"./term.js":"src/term.js"},"src/query.js":{"./parser.js":"src/parser.js","./store.js":"src/store.js","./engine.js":"src/engine.js","./backward.js":"src/backward.js","./api.js":"src/api.js"},"src/output.js":{},"src/api.js":{"./parser.js":"src/parser.js","./rdfSyntax.js":"src/rdfSyntax.js","./rdfMessages.js":"src/rdfMessages.js","./engine.js":"src/engine.js","./analyze.js":"src/analyze.js","./format.js":"src/format.js","./query.js":"src/query.js","./output.js":"src/output.js"},"src/cli.js":{"./api.js":"src/api.js","./term.js":"src/term.js"}};
|
|
5812
6523
|
const __cache = {};
|
|
5813
6524
|
function __require(id) {
|
|
5814
6525
|
if (!id.startsWith("src/")) return __nativeRequire(id);
|