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.
@@ -16,7 +16,7 @@
16
16
  const { evaluate } = require('./engine.js');
17
17
  const { analyze } = require('./analyze.js');
18
18
  const { formatTriples, sortTriples, toJSON, formatTrace, formatBindings } = require('./format.js');
19
- const { runQuery, queryResult } = require('./query.js');
19
+ const { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery } = require('./query.js');
20
20
  const { resultTriples } = require('./output.js');
21
21
 
22
22
  function parseInput(source, options = {}) {
@@ -130,6 +130,9 @@
130
130
  runToString,
131
131
  runQuery,
132
132
  queryResult,
133
+ queryProgram,
134
+ queryRunOptions,
135
+ shouldUseHybridForQuery,
133
136
  formatTriples,
134
137
  formatBindings,
135
138
  sortTriples,
@@ -4048,10 +4051,11 @@
4048
4051
  "src/engine.js": function (require, module, exports) {
4049
4052
  'use strict';
4050
4053
 
4051
- const { TripleStore, bindingKey } = require('./store.js');
4054
+ const { TripleStore, bindingKey, instantiateTerm } = require('./store.js');
4052
4055
  const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
4053
4056
  const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
4054
4057
  const { analyze } = require('./analyze.js');
4058
+ const { BackwardProver, preferredBackwardPredicates, ruleIsBackwardOriented } = require('./backward.js');
4055
4059
 
4056
4060
  function evaluate(program, options = {}) {
4057
4061
  const maxIterations = options.maxIterations ?? 10000;
@@ -4067,6 +4071,7 @@
4067
4071
  applications: 0,
4068
4072
  added: 0,
4069
4073
  runOnce: !!rule.runOnce,
4074
+ backward: false,
4070
4075
  }));
4071
4076
 
4072
4077
  const analysis = options.analysis || analyze(program, options);
@@ -4083,6 +4088,17 @@
4083
4088
  const relaxedRecursiveRunOnce = options.relaxedRecursion === false
4084
4089
  ? new Set()
4085
4090
  : recursiveTermGenerationRuleIndexes(analysis);
4091
+ const hybridBackwardPredicates = options.hybrid || options.backwardBodyCalls
4092
+ ? preferredBackwardPredicates(program, options)
4093
+ : new Set();
4094
+ const hybridBackwardRules = new Set();
4095
+ if (hybridBackwardPredicates.size > 0) {
4096
+ for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {
4097
+ if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);
4098
+ }
4099
+ }
4100
+ const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;
4101
+ for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;
4086
4102
  const baseContext = {
4087
4103
  ...evalOptions,
4088
4104
  maxIterations,
@@ -4094,12 +4110,16 @@
4094
4110
  iteration: 0,
4095
4111
  startingIterations: 0,
4096
4112
  recursiveLayer: false,
4113
+ hybridBackwardPredicates,
4114
+ hybridBackwardRules,
4115
+ hybridStats,
4097
4116
  };
4098
4117
 
4099
4118
  for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
4100
4119
  const layer = layerIndexes[layerIndex];
4101
- const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
4102
- const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
4120
+ const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;
4121
+ const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
4122
+ const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
4103
4123
 
4104
4124
  if (runOnce.length > 0) {
4105
4125
  iterations += 1;
@@ -4132,6 +4152,7 @@
4132
4152
  ruleApplications,
4133
4153
  perRule,
4134
4154
  trace,
4155
+ hybridStats,
4135
4156
  };
4136
4157
  }
4137
4158
 
@@ -4201,9 +4222,10 @@
4201
4222
  const seenBindings = dedupeBindings ? new Set() : null;
4202
4223
  const headBlankLabels = collectHeadBlankLabels(rule.head);
4203
4224
 
4204
- const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
4225
+ const bodyContext = prepareBodyContext(program, store, context);
4226
+ const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)
4205
4227
  ? store.match(rule.body[0].triple, {})
4206
- : evaluateBodyStream(rule.body, store, {}, context);
4228
+ : evaluateBodyStream(rule.body, store, {}, bodyContext);
4207
4229
 
4208
4230
  for (const binding of bodyBindings) {
4209
4231
  if (seenBindings) {
@@ -4236,9 +4258,36 @@
4236
4258
  }
4237
4259
  }
4238
4260
 
4261
+ if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);
4239
4262
  return { applications, added };
4240
4263
  }
4241
4264
 
4265
+ function prepareBodyContext(program, store, context) {
4266
+ if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
4267
+ return {
4268
+ ...context,
4269
+ backwardProver: new BackwardProver(program, {
4270
+ ...context,
4271
+ store,
4272
+ allowedPredicates: context.hybridBackwardPredicates,
4273
+ }),
4274
+ };
4275
+ }
4276
+
4277
+ function emptyBackwardStats() {
4278
+ return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };
4279
+ }
4280
+
4281
+ function mergeBackwardStats(total, item) {
4282
+ if (!total || !item) return;
4283
+ total.goals += item.goals || 0;
4284
+ total.facts += item.facts || 0;
4285
+ total.rules += item.rules || 0;
4286
+ total.memoHits += item.memoHits || 0;
4287
+ total.memoStores += item.memoStores || 0;
4288
+ total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);
4289
+ }
4290
+
4242
4291
  function recursiveTermGenerationRuleIndexes(analysis) {
4243
4292
  const out = new Set();
4244
4293
  if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
@@ -4347,9 +4396,20 @@
4347
4396
 
4348
4397
  const clause = clauses[index];
4349
4398
  if (clause.type === 'triple') {
4399
+ const seen = new Set();
4350
4400
  for (const matched of store.match(clause.triple, initialBinding)) {
4401
+ const key = bindingKey(matched);
4402
+ seen.add(key);
4351
4403
  yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
4352
4404
  }
4405
+ if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {
4406
+ for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {
4407
+ const key = bindingKey(matched);
4408
+ if (seen.has(key)) continue;
4409
+ seen.add(key);
4410
+ yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
4411
+ }
4412
+ }
4353
4413
  return;
4354
4414
  }
4355
4415
 
@@ -4395,6 +4455,12 @@
4395
4455
  throw new Error(`Unsupported body clause ${clause.type}`);
4396
4456
  }
4397
4457
 
4458
+ function shouldUseBackwardForTriple(pattern, binding, options = {}) {
4459
+ if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;
4460
+ const predicate = instantiateTerm(pattern.p, binding);
4461
+ return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));
4462
+ }
4463
+
4398
4464
  function bodyHasAny(clauses, store, initialBinding, options) {
4399
4465
  for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;
4400
4466
  return false;
@@ -4427,6 +4493,7 @@
4427
4493
  this.byPredicate = new Map();
4428
4494
  this.byPredicateSubject = new Map();
4429
4495
  this.byPredicateObject = new Map();
4496
+ this.version = 0;
4430
4497
  for (const triple of triples) this.add(triple);
4431
4498
  }
4432
4499
 
@@ -4441,6 +4508,7 @@
4441
4508
  addIndex(this.byPredicate, predicate, key, normalized);
4442
4509
  addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);
4443
4510
  addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);
4511
+ this.version += 1;
4444
4512
  return true;
4445
4513
  }
4446
4514
 
@@ -5422,6 +5490,499 @@
5422
5490
  canPossiblyGenerate,
5423
5491
  };
5424
5492
 
5493
+ },
5494
+ "src/backward.js": function (require, module, exports) {
5495
+ 'use strict';
5496
+
5497
+ const { TripleStore, bindingKey } = require('./store.js');
5498
+ const { tripleKey, termKey, termEquals } = require('./term.js');
5499
+ const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
5500
+
5501
+ function backwardQuery(program, querySpec, options = {}) {
5502
+ const planner = planBackwardQuery(program, querySpec, options);
5503
+ if (!planner.ok) return { ok: false, reason: planner.reason };
5504
+ const prover = new BackwardProver(program, { ...options, allowedRuleIndexes: planner.ruleIndexes });
5505
+ const bindings = uniqueBindings(Array.from(prover.solveBody(querySpec.body, {})));
5506
+ return { ok: true, bindings, stats: prover.stats, plan: planner };
5507
+ }
5508
+
5509
+ function planBackwardQuery(program, querySpec, options = {}) {
5510
+ const clauses = querySpec && Array.isArray(querySpec.body) ? querySpec.body : [];
5511
+ if (!bodySupported(clauses, options)) return { ok: false, reason: 'query body contains clauses not supported by the backward prover yet' };
5512
+
5513
+ const reachable = reachableBackwardRuleIndexes(program, clauses, options);
5514
+ for (const ruleIndex of reachable.ruleIndexes) {
5515
+ const rule = (program.rules || [])[ruleIndex];
5516
+ if (!ruleSupported(rule, options)) return { ok: false, reason: `reachable rule ${rule.name || '<anonymous>'} is not supported by the backward prover yet` };
5517
+ }
5518
+ return { ok: true, ruleIndexes: reachable.ruleIndexes, predicates: reachable.predicates };
5519
+ }
5520
+
5521
+ function bodySupported(clauses, options = {}) {
5522
+ for (const clause of clauses || []) {
5523
+ if (clause.type === 'triple' || clause.type === 'filter' || clause.type === 'set' || clause.type === 'bind') continue;
5524
+ if (clause.type === 'not') {
5525
+ if (options.backwardNegation === false) return false;
5526
+ if (!bodySupported(clause.body, options)) return false;
5527
+ continue;
5528
+ }
5529
+ return false;
5530
+ }
5531
+ return true;
5532
+ }
5533
+
5534
+ function ruleSupported(rule, options = {}) {
5535
+ if (!Array.isArray(rule.head) || rule.head.length === 0) return false;
5536
+ for (const head of rule.head) {
5537
+ if (!head || !head.p || head.p.type !== 'iri') return false;
5538
+ if (containsBlank(head.s) || containsBlank(head.p) || containsBlank(head.o)) return false;
5539
+ }
5540
+ return bodySupported(rule.body || [], options);
5541
+ }
5542
+
5543
+ class BackwardProver {
5544
+ constructor(program, options = {}) {
5545
+ this.program = program;
5546
+ this.options = options;
5547
+ this.store = options.store || new TripleStore(program.data || []);
5548
+ this.maxDepth = options.backwardMaxDepth || options.maxDepth || 10000;
5549
+ this.solutionLimit = options.backwardSolutionLimit || options.solutionLimit || 1000000;
5550
+ this.allowedPredicates = normalizePredicateSet(options.allowedPredicates || options.backwardPredicates || null);
5551
+ this.allowedRuleIndexes = normalizeRuleIndexSet(options.allowedRuleIndexes || null);
5552
+ this.ruleHeads = indexRuleHeads(program.rules || [], { allowedPredicates: this.allowedPredicates, allowedRuleIndexes: this.allowedRuleIndexes });
5553
+ this.memo = new Map();
5554
+ this.active = new Set();
5555
+ this.freshCounter = 0;
5556
+ this.solutionCount = 0;
5557
+ this.stats = {
5558
+ mode: 'backward',
5559
+ goals: 0,
5560
+ facts: 0,
5561
+ rules: 0,
5562
+ memoHits: 0,
5563
+ memoStores: 0,
5564
+ maxDepth: 0,
5565
+ };
5566
+ }
5567
+
5568
+ *solveBody(clauses, binding = {}, depth = 0, index = 0) {
5569
+ if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);
5570
+ this.stats.maxDepth = Math.max(this.stats.maxDepth, depth);
5571
+ if (this.solutionCount >= this.solutionLimit) return;
5572
+ if (index >= clauses.length) {
5573
+ this.solutionCount += 1;
5574
+ yield resolveBinding(binding);
5575
+ return;
5576
+ }
5577
+
5578
+ const clause = clauses[index];
5579
+ if (clause.type === 'triple') {
5580
+ for (const matched of this.solveTriple(clause.triple, binding, depth + 1)) {
5581
+ yield* this.solveBody(clauses, matched, depth + 1, index + 1);
5582
+ }
5583
+ return;
5584
+ }
5585
+
5586
+ if (clause.type === 'filter') {
5587
+ try {
5588
+ if (booleanValue(evalExpression(clause.expr, resolveBinding(binding), this.options))) {
5589
+ yield* this.solveBody(clauses, binding, depth + 1, index + 1);
5590
+ }
5591
+ } catch (_) {
5592
+ // SPARQL-style FILTER errors reject the current solution.
5593
+ }
5594
+ return;
5595
+ }
5596
+
5597
+ if (clause.type === 'set' || clause.type === 'bind') {
5598
+ try {
5599
+ const resolved = resolveBinding(binding);
5600
+ const value = asTerm(evalExpression(clause.expr, resolved, this.options));
5601
+ const next = unifyTerms({ type: 'var', value: clause.variable }, value, binding);
5602
+ if (next) yield* this.solveBody(clauses, next, depth + 1, index + 1);
5603
+ } catch (_) {
5604
+ // Assignment errors drop the current solution.
5605
+ }
5606
+ return;
5607
+ }
5608
+
5609
+ if (clause.type === 'not') {
5610
+ let found = false;
5611
+ for (const _ of this.solveBody(clause.body, { ...binding }, depth + 1, 0)) { found = true; break; }
5612
+ if (!found) yield* this.solveBody(clauses, binding, depth + 1, index + 1);
5613
+ return;
5614
+ }
5615
+
5616
+ throw new Error(`Unsupported backward body clause ${clause.type}`);
5617
+ }
5618
+
5619
+ *solveTriple(pattern, binding = {}, depth = 0) {
5620
+ if (depth > this.maxDepth) throw new Error(`Reached backwardMaxDepth=${this.maxDepth}; backward query may not terminate`);
5621
+ this.stats.goals += 1;
5622
+ const resolvedPattern = resolvePattern(pattern, binding);
5623
+ const key = `${goalKey(resolvedPattern)}@store:${this.store.version || 0}`;
5624
+ const entry = this.memo.get(key);
5625
+ if (entry && entry.complete) {
5626
+ this.stats.memoHits += 1;
5627
+ for (const answer of entry.answers) {
5628
+ const next = unifyTriples(pattern, answer, binding);
5629
+ if (next) yield next;
5630
+ }
5631
+ return;
5632
+ }
5633
+ if (this.active.has(key)) return;
5634
+
5635
+ const answers = [];
5636
+ const answerKeys = new Set();
5637
+ this.active.add(key);
5638
+ try {
5639
+ for (const fact of this.factCandidates(resolvedPattern, binding)) {
5640
+ const next = unifyTriples(pattern, fact, binding);
5641
+ if (!next) continue;
5642
+ rememberAnswer(answers, answerKeys, pattern, next);
5643
+ this.stats.facts += 1;
5644
+ yield next;
5645
+ }
5646
+
5647
+ for (const item of this.ruleCandidates(resolvedPattern)) {
5648
+ const suffix = `__b${++this.freshCounter}_${item.ruleIndex}`;
5649
+ const freshHead = freshTriple(item.head, suffix);
5650
+ const next = unifyTriples(pattern, freshHead, binding);
5651
+ if (!next) continue;
5652
+ const freshBody = (item.rule.body || []).map((clause) => freshClause(clause, suffix));
5653
+ for (const solved of this.solveBody(freshBody, next, depth + 1, 0)) {
5654
+ rememberAnswer(answers, answerKeys, pattern, solved);
5655
+ this.stats.rules += 1;
5656
+ yield solved;
5657
+ }
5658
+ }
5659
+ } finally {
5660
+ this.active.delete(key);
5661
+ }
5662
+
5663
+ this.memo.set(key, { complete: true, answers });
5664
+ this.stats.memoStores += 1;
5665
+ }
5666
+
5667
+ factCandidates(pattern, binding) {
5668
+ return this.store.candidates(pattern, binding);
5669
+ }
5670
+
5671
+ ruleCandidates(pattern) {
5672
+ const predicate = pattern.p && pattern.p.type === 'iri' ? pattern.p.value : null;
5673
+ if (predicate) return this.ruleHeads.byPredicate.get(predicate) || [];
5674
+ return this.ruleHeads.all;
5675
+ }
5676
+ }
5677
+
5678
+ function indexRuleHeads(rules, options = {}) {
5679
+ const allowedPredicates = options.allowedPredicates || null;
5680
+ const allowedRuleIndexes = options.allowedRuleIndexes || null;
5681
+ const byPredicate = new Map();
5682
+ const all = [];
5683
+ for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
5684
+ if (allowedRuleIndexes && !allowedRuleIndexes.has(ruleIndex)) continue;
5685
+ const rule = rules[ruleIndex];
5686
+ for (let headIndex = 0; headIndex < (rule.head || []).length; headIndex += 1) {
5687
+ const head = rule.head[headIndex];
5688
+ if (!head || !head.p || head.p.type !== 'iri') continue;
5689
+ if (allowedPredicates && !allowedPredicates.has(head.p.value)) continue;
5690
+ const item = { ruleIndex, headIndex, rule, head };
5691
+ all.push(item);
5692
+ const bucket = byPredicate.get(head.p.value);
5693
+ if (bucket) bucket.push(item);
5694
+ else byPredicate.set(head.p.value, [item]);
5695
+ }
5696
+ }
5697
+ return { byPredicate, all };
5698
+ }
5699
+
5700
+ function freshClause(clause, suffix) {
5701
+ if (clause.type === 'triple') return { ...clause, triple: freshTriple(clause.triple, suffix) };
5702
+ if (clause.type === 'filter') return { ...clause, expr: freshExpr(clause.expr, suffix) };
5703
+ if (clause.type === 'set' || clause.type === 'bind') return { ...clause, variable: freshVarName(clause.variable, suffix), expr: freshExpr(clause.expr, suffix) };
5704
+ if (clause.type === 'not') return { ...clause, body: clause.body.map((item) => freshClause(item, suffix)) };
5705
+ return clause;
5706
+ }
5707
+
5708
+ function freshTriple(triple, suffix) {
5709
+ return { s: freshTerm(triple.s, suffix), p: freshTerm(triple.p, suffix), o: freshTerm(triple.o, suffix) };
5710
+ }
5711
+
5712
+ function freshTerm(term, suffix) {
5713
+ if (!term) return term;
5714
+ if (term.type === 'var') return { type: 'var', value: freshVarName(term.value, suffix) };
5715
+ if (term.type === 'triple') return { type: 'triple', s: freshTerm(term.s, suffix), p: freshTerm(term.p, suffix), o: freshTerm(term.o, suffix) };
5716
+ return term;
5717
+ }
5718
+
5719
+ function freshExpr(expr, suffix) {
5720
+ if (!expr) return expr;
5721
+ if (expr.type === 'var') return { ...expr, name: freshVarName(expr.name, suffix) };
5722
+ if (expr.type === 'term') return { ...expr, value: freshTerm(expr.value, suffix) };
5723
+ if (expr.type === 'unary') return { ...expr, expr: freshExpr(expr.expr, suffix) };
5724
+ if (expr.type === 'binary') return { ...expr, left: freshExpr(expr.left, suffix), right: freshExpr(expr.right, suffix) };
5725
+ if (expr.type === 'call') return { ...expr, args: expr.args.map((arg) => freshExpr(arg, suffix)) };
5726
+ if (expr.type === 'list') return { ...expr, items: expr.items.map((item) => freshExpr(item, suffix)) };
5727
+ return expr;
5728
+ }
5729
+
5730
+ function freshVarName(name, suffix) {
5731
+ return `${name}${suffix}`;
5732
+ }
5733
+
5734
+ function resolvePattern(pattern, binding) {
5735
+ return { s: resolveTerm(pattern.s, binding, false), p: resolveTerm(pattern.p, binding, false), o: resolveTerm(pattern.o, binding, false) };
5736
+ }
5737
+
5738
+ function resolveBinding(binding) {
5739
+ const out = {};
5740
+ for (const name of Object.keys(binding)) out[name] = resolveTerm(binding[name], binding, false);
5741
+ return out;
5742
+ }
5743
+
5744
+ function resolveTerm(term, binding, preserveUnbound = true, seen = new Set()) {
5745
+ if (!term) return term;
5746
+ if (term.type === 'var') {
5747
+ const name = term.value;
5748
+ if (seen.has(name)) return preserveUnbound ? term : { type: 'var', value: name };
5749
+ if (!Object.prototype.hasOwnProperty.call(binding, name)) return preserveUnbound ? term : { type: 'var', value: name };
5750
+ seen.add(name);
5751
+ return resolveTerm(binding[name], binding, preserveUnbound, seen);
5752
+ }
5753
+ if (term.type === 'triple') {
5754
+ return {
5755
+ type: 'triple',
5756
+ s: resolveTerm(term.s, binding, preserveUnbound, new Set(seen)),
5757
+ p: resolveTerm(term.p, binding, preserveUnbound, new Set(seen)),
5758
+ o: resolveTerm(term.o, binding, preserveUnbound, new Set(seen)),
5759
+ };
5760
+ }
5761
+ return term;
5762
+ }
5763
+
5764
+ function unifyTriples(left, right, binding) {
5765
+ let next = unifyTerms(left.s, right.s, binding);
5766
+ if (!next) return null;
5767
+ next = unifyTerms(left.p, right.p, next);
5768
+ if (!next) return null;
5769
+ return unifyTerms(left.o, right.o, next);
5770
+ }
5771
+
5772
+ function unifyTerms(left, right, binding) {
5773
+ const a = resolveTerm(left, binding);
5774
+ const b = resolveTerm(right, binding);
5775
+ if (a.type === 'var' && b.type === 'var' && a.value === b.value) return binding;
5776
+ if (a.type === 'var') return bindVariable(a.value, b, binding);
5777
+ if (b.type === 'var') return bindVariable(b.value, a, binding);
5778
+ if (a.type === 'triple' || b.type === 'triple') {
5779
+ if (a.type !== 'triple' || b.type !== 'triple') return null;
5780
+ let next = unifyTerms(a.s, b.s, binding);
5781
+ if (!next) return null;
5782
+ next = unifyTerms(a.p, b.p, next);
5783
+ if (!next) return null;
5784
+ return unifyTerms(a.o, b.o, next);
5785
+ }
5786
+ return termEquals(a, b) ? binding : null;
5787
+ }
5788
+
5789
+ function bindVariable(name, term, binding) {
5790
+ const existing = binding[name];
5791
+ if (existing) return unifyTerms(existing, term, binding);
5792
+ if (term.type === 'var' && term.value === name) return binding;
5793
+ return { ...binding, [name]: term };
5794
+ }
5795
+
5796
+ function rememberAnswer(answers, answerKeys, pattern, binding) {
5797
+ const triple = {
5798
+ s: resolveTerm(pattern.s, binding),
5799
+ p: resolveTerm(pattern.p, binding),
5800
+ o: resolveTerm(pattern.o, binding),
5801
+ };
5802
+ if (triple.s.type === 'var' || triple.p.type === 'var' || triple.o.type === 'var') return;
5803
+ const key = tripleKey(triple);
5804
+ if (answerKeys.has(key)) return;
5805
+ answerKeys.add(key);
5806
+ answers.push(triple);
5807
+ }
5808
+
5809
+ function goalKey(pattern) {
5810
+ return `${safeTermKey(pattern.s)} ${safeTermKey(pattern.p)} ${safeTermKey(pattern.o)}`;
5811
+ }
5812
+
5813
+ function safeTermKey(term) {
5814
+ return term && term.type === 'var' ? '_' : termKey(term);
5815
+ }
5816
+
5817
+ function uniqueBindings(bindings) {
5818
+ const seen = new Set();
5819
+ const out = [];
5820
+ for (const binding of bindings) {
5821
+ const resolved = resolveBinding(binding);
5822
+ const key = bindingKey(resolved);
5823
+ if (seen.has(key)) continue;
5824
+ seen.add(key);
5825
+ out.push(resolved);
5826
+ }
5827
+ return out;
5828
+ }
5829
+
5830
+ function containsBlank(term) {
5831
+ if (!term) return false;
5832
+ if (term.type === 'blank') return true;
5833
+ if (term.type === 'triple') return containsBlank(term.s) || containsBlank(term.p) || containsBlank(term.o);
5834
+ return false;
5835
+ }
5836
+
5837
+
5838
+
5839
+ function reachableBackwardRuleIndexes(program, rootClauses, options = {}) {
5840
+ const rules = program.rules || [];
5841
+ const headIndex = new Map();
5842
+ const allHeadPredicates = new Set();
5843
+ const allRuleIndexes = new Set();
5844
+ for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex += 1) {
5845
+ const rule = rules[ruleIndex];
5846
+ for (const head of rule.head || []) {
5847
+ if (!head || !head.p || head.p.type !== 'iri') continue;
5848
+ allRuleIndexes.add(ruleIndex);
5849
+ allHeadPredicates.add(head.p.value);
5850
+ if (!headIndex.has(head.p.value)) headIndex.set(head.p.value, new Set());
5851
+ headIndex.get(head.p.value).add(ruleIndex);
5852
+ }
5853
+ }
5854
+
5855
+ const predicates = new Set();
5856
+ const ruleIndexes = new Set();
5857
+ const work = [];
5858
+ const enqueue = (predicate) => {
5859
+ if (!predicate) {
5860
+ for (const item of allHeadPredicates) enqueue(item);
5861
+ return;
5862
+ }
5863
+ if (predicates.has(predicate)) return;
5864
+ predicates.add(predicate);
5865
+ work.push(predicate);
5866
+ };
5867
+
5868
+ for (const predicate of bodyPredicateDemands(rootClauses || [])) enqueue(predicate);
5869
+
5870
+ while (work.length > 0) {
5871
+ const predicate = work.shift();
5872
+ const indexes = headIndex.get(predicate);
5873
+ if (!indexes) continue;
5874
+ for (const ruleIndex of indexes) {
5875
+ if (ruleIndexes.has(ruleIndex)) continue;
5876
+ ruleIndexes.add(ruleIndex);
5877
+ const rule = rules[ruleIndex];
5878
+ for (const needed of bodyPredicateDemands(rule.body || [])) enqueue(needed);
5879
+ }
5880
+ }
5881
+
5882
+ return { ruleIndexes, predicates };
5883
+ }
5884
+
5885
+ function bodyPredicateDemands(clauses) {
5886
+ const out = [];
5887
+ for (const clause of clauses || []) {
5888
+ if (clause.type === 'triple') {
5889
+ out.push(predicateDemand(clause.triple.p));
5890
+ continue;
5891
+ }
5892
+ if (clause.type === 'not') {
5893
+ out.push(...bodyPredicateDemands(clause.body || []));
5894
+ }
5895
+ }
5896
+ return out;
5897
+ }
5898
+
5899
+ function predicateDemand(term) {
5900
+ return term && term.type === 'iri' ? term.value : null;
5901
+ }
5902
+
5903
+ function normalizePredicateSet(value) {
5904
+ if (!value) return null;
5905
+ if (value instanceof Set) return value;
5906
+ if (Array.isArray(value)) return new Set(value);
5907
+ return new Set(String(value).split(',').map((item) => item.trim()).filter(Boolean));
5908
+ }
5909
+
5910
+ function normalizeRuleIndexSet(value) {
5911
+ if (!value) return null;
5912
+ if (value instanceof Set) return value;
5913
+ if (Array.isArray(value)) return new Set(value);
5914
+ return null;
5915
+ }
5916
+
5917
+ function supportedBackwardPredicates(program, options = {}) {
5918
+ const explicit = normalizePredicateSet(options.backwardPredicates || options.hybridPredicates || null);
5919
+ const byPredicate = new Map();
5920
+ for (const rule of program.rules || []) {
5921
+ const predicates = new Set();
5922
+ for (const head of rule.head || []) {
5923
+ if (head && head.p && head.p.type === 'iri') predicates.add(head.p.value);
5924
+ }
5925
+ for (const predicate of predicates) {
5926
+ if (!byPredicate.has(predicate)) byPredicate.set(predicate, []);
5927
+ byPredicate.get(predicate).push(rule);
5928
+ }
5929
+ }
5930
+
5931
+ const supported = new Set();
5932
+ for (const [predicate, rules] of byPredicate) {
5933
+ if (explicit && !explicit.has(predicate)) continue;
5934
+ if (rules.length > 0 && rules.every((rule) => ruleSupported(rule, options))) supported.add(predicate);
5935
+ }
5936
+ return supported;
5937
+ }
5938
+
5939
+ function preferredBackwardPredicates(program, options = {}) {
5940
+ const explicit = normalizePredicateSet(options.hybridPredicates || null);
5941
+ if (explicit) return supportedBackwardPredicates(program, { ...options, hybridPredicates: explicit });
5942
+ const supported = supportedBackwardPredicates(program, options);
5943
+ const preferred = new Set();
5944
+ for (const rule of program.rules || []) {
5945
+ if (!ruleIsFunctionLike(rule)) continue;
5946
+ for (const head of rule.head || []) {
5947
+ if (head && head.p && head.p.type === 'iri' && supported.has(head.p.value)) preferred.add(head.p.value);
5948
+ }
5949
+ }
5950
+ return preferred;
5951
+ }
5952
+
5953
+ function ruleIsFunctionLike(rule) {
5954
+ return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
5955
+ }
5956
+
5957
+ function ruleHeadPredicates(rule) {
5958
+ const predicates = new Set();
5959
+ for (const head of rule.head || []) {
5960
+ if (!head || !head.p || head.p.type !== 'iri') return null;
5961
+ predicates.add(head.p.value);
5962
+ }
5963
+ return predicates;
5964
+ }
5965
+
5966
+ function ruleIsBackwardOriented(rule, predicates) {
5967
+ if (!predicates || predicates.size === 0) return false;
5968
+ const heads = ruleHeadPredicates(rule);
5969
+ if (!heads || heads.size === 0) return false;
5970
+ for (const predicate of heads) if (!predicates.has(predicate)) return false;
5971
+ return true;
5972
+ }
5973
+
5974
+ module.exports = {
5975
+ BackwardProver,
5976
+ backwardQuery,
5977
+ planBackwardQuery,
5978
+ supportedBackwardPredicates,
5979
+ preferredBackwardPredicates,
5980
+ reachableBackwardRuleIndexes,
5981
+ ruleIsBackwardOriented,
5982
+ ruleSupported,
5983
+ resolveBinding,
5984
+ };
5985
+
5425
5986
  },
5426
5987
  "src/format.js": function (require, module, exports) {
5427
5988
  'use strict';
@@ -5515,6 +6076,7 @@
5515
6076
  const { parseQuery } = require('./parser.js');
5516
6077
  const { TripleStore, bindingKey } = require('./store.js');
5517
6078
  const { evaluateBody } = require('./engine.js');
6079
+ const { backwardQuery, planBackwardQuery, preferredBackwardPredicates } = require('./backward.js');
5518
6080
 
5519
6081
  function queryResult(result, querySpec, options = {}) {
5520
6082
  const store = new TripleStore(result.closure || []);
@@ -5525,28 +6087,89 @@
5525
6087
  prefixes: result.prefixes,
5526
6088
  select,
5527
6089
  bindings: projectBindings(bindings, select),
6090
+ mode: options.queryMode === 'hybrid' || options.hybrid ? 'hybrid' : 'forward',
5528
6091
  };
5529
6092
  }
5530
6093
 
6094
+ function queryProgram(program, querySpec, options = {}) {
6095
+ const mode = options.queryMode || 'auto';
6096
+ if (mode !== 'forward' && mode !== 'hybrid') {
6097
+ const planned = planBackwardQuery(program, querySpec, options);
6098
+ if (planned.ok) {
6099
+ const result = backwardQuery(program, querySpec, options);
6100
+ if (result.ok) {
6101
+ const select = normalizeSelect(querySpec.select, result.bindings);
6102
+ return {
6103
+ baseIRI: program.baseIRI,
6104
+ prefixes: program.prefixes,
6105
+ select,
6106
+ bindings: projectBindings(result.bindings, select),
6107
+ mode: 'backward',
6108
+ stats: result.stats,
6109
+ };
6110
+ }
6111
+ if (mode === 'backward') throw new Error(result.reason || 'Backward query failed');
6112
+ } else if (mode === 'backward') {
6113
+ throw new Error(`Backward query is not supported for this ruleset: ${planned.reason}`);
6114
+ }
6115
+ }
6116
+ return null;
6117
+ }
6118
+
5531
6119
  function runQuery(source, querySource = null, options = {}) {
5532
6120
  const { run, compile } = require('./api.js');
5533
- const { program, diagnostics } = compile(source, options);
5534
- const result = run(program, options);
5535
- result.diagnostics = diagnostics;
6121
+ const { program, diagnostics, analysis } = compile(source, options);
5536
6122
 
5537
6123
  let querySpec;
5538
6124
  if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });
5539
6125
  else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');
5540
6126
 
5541
- const query = queryResult(result, querySpec, options);
5542
- return { ...result, query };
6127
+ const direct = queryProgram(program, querySpec, options);
6128
+ if (direct) {
6129
+ return {
6130
+ baseIRI: program.baseIRI,
6131
+ version: program.version || null,
6132
+ imports: program.imports || [],
6133
+ prefixes: program.prefixes,
6134
+ input: program.data.slice(),
6135
+ inferred: [],
6136
+ closure: program.data.slice(),
6137
+ iterations: 0,
6138
+ layers: [],
6139
+ ruleApplications: 0,
6140
+ perRule: [],
6141
+ trace: [],
6142
+ diagnostics,
6143
+ analysis,
6144
+ query: direct,
6145
+ };
6146
+ }
6147
+
6148
+ const runOptions = queryRunOptions(program, querySpec, options);
6149
+ const result = run(program, runOptions);
6150
+ result.diagnostics = diagnostics;
6151
+ result.query = queryResult(result, querySpec, runOptions);
6152
+ return result;
6153
+ }
6154
+
6155
+ function queryRunOptions(program, querySpec, options = {}) {
6156
+ if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: true };
6157
+ return options;
6158
+ }
6159
+
6160
+ function shouldUseHybridForQuery(program, querySpec, options = {}) {
6161
+ const mode = options.queryMode || 'auto';
6162
+ if (options.hybrid || mode === 'hybrid') return true;
6163
+ if (mode !== 'auto') return false;
6164
+ if (!querySpec) return false;
6165
+ return preferredBackwardPredicates(program, options).size > 0;
5543
6166
  }
5544
6167
 
5545
6168
  function normalizeSelect(select, bindings) {
5546
6169
  if (select && select.length > 0) return select.slice();
5547
6170
  const vars = new Set();
5548
6171
  for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);
5549
- return Array.from(vars).sort();
6172
+ return Array.from(vars).sort().filter((name) => !name.includes('__b'));
5550
6173
  }
5551
6174
 
5552
6175
  function projectBindings(bindings, select) {
@@ -5564,7 +6187,7 @@
5564
6187
  return out;
5565
6188
  }
5566
6189
 
5567
- module.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };
6190
+ module.exports = { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery, parseQuery, normalizeSelect, projectBindings };
5568
6191
 
5569
6192
  },
5570
6193
  "src/output.js": function (require, module, exports) {
@@ -5578,7 +6201,7 @@
5578
6201
 
5579
6202
  },
5580
6203
  };
5581
- 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/index.js":{"./api.js":"src/api.js"}};
6204
+ 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/index.js":{"./api.js":"src/api.js"}};
5582
6205
  const __cache = {};
5583
6206
  function __require(id) {
5584
6207
  if (__cache[id]) return __cache[id].exports;