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/src/engine.js CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const { TripleStore, bindingKey } = require('./store.js');
3
+ const { TripleStore, bindingKey, instantiateTerm } = require('./store.js');
4
4
  const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
5
5
  const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
6
6
  const { analyze } = require('./analyze.js');
7
+ const { BackwardProver, preferredBackwardPredicates, ruleIsBackwardOriented } = require('./backward.js');
7
8
 
8
9
  function evaluate(program, options = {}) {
9
10
  const maxIterations = options.maxIterations ?? 10000;
@@ -19,6 +20,7 @@ function evaluate(program, options = {}) {
19
20
  applications: 0,
20
21
  added: 0,
21
22
  runOnce: !!rule.runOnce,
23
+ backward: false,
22
24
  }));
23
25
 
24
26
  const analysis = options.analysis || analyze(program, options);
@@ -35,6 +37,17 @@ function evaluate(program, options = {}) {
35
37
  const relaxedRecursiveRunOnce = options.relaxedRecursion === false
36
38
  ? new Set()
37
39
  : recursiveTermGenerationRuleIndexes(analysis);
40
+ const hybridBackwardPredicates = options.hybrid || options.backwardBodyCalls
41
+ ? preferredBackwardPredicates(program, options)
42
+ : new Set();
43
+ const hybridBackwardRules = new Set();
44
+ if (hybridBackwardPredicates.size > 0) {
45
+ for (let ruleIndex = 0; ruleIndex < program.rules.length; ruleIndex += 1) {
46
+ if (ruleIsBackwardOriented(program.rules[ruleIndex], hybridBackwardPredicates)) hybridBackwardRules.add(ruleIndex);
47
+ }
48
+ }
49
+ const hybridStats = hybridBackwardPredicates.size > 0 ? emptyBackwardStats() : null;
50
+ for (const ruleIndex of hybridBackwardRules) perRule[ruleIndex].backward = true;
38
51
  const baseContext = {
39
52
  ...evalOptions,
40
53
  maxIterations,
@@ -46,12 +59,16 @@ function evaluate(program, options = {}) {
46
59
  iteration: 0,
47
60
  startingIterations: 0,
48
61
  recursiveLayer: false,
62
+ hybridBackwardPredicates,
63
+ hybridBackwardRules,
64
+ hybridStats,
49
65
  };
50
66
 
51
67
  for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
52
68
  const layer = layerIndexes[layerIndex];
53
- const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
54
- const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
69
+ const forwardLayer = hybridBackwardRules.size > 0 ? layer.filter((ruleIndex) => !hybridBackwardRules.has(ruleIndex)) : layer;
70
+ const ordinary = forwardLayer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
71
+ const runOnce = forwardLayer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
55
72
 
56
73
  if (runOnce.length > 0) {
57
74
  iterations += 1;
@@ -84,6 +101,7 @@ function evaluate(program, options = {}) {
84
101
  ruleApplications,
85
102
  perRule,
86
103
  trace,
104
+ hybridStats,
87
105
  };
88
106
  }
89
107
 
@@ -153,9 +171,10 @@ function applyRuleOnce(program, store, ruleIndex, context) {
153
171
  const seenBindings = dedupeBindings ? new Set() : null;
154
172
  const headBlankLabels = collectHeadBlankLabels(rule.head);
155
173
 
156
- const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
174
+ const bodyContext = prepareBodyContext(program, store, context);
175
+ const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple' && !shouldUseBackwardForTriple(rule.body[0].triple, {}, bodyContext)
157
176
  ? store.match(rule.body[0].triple, {})
158
- : evaluateBodyStream(rule.body, store, {}, context);
177
+ : evaluateBodyStream(rule.body, store, {}, bodyContext);
159
178
 
160
179
  for (const binding of bodyBindings) {
161
180
  if (seenBindings) {
@@ -188,9 +207,36 @@ function applyRuleOnce(program, store, ruleIndex, context) {
188
207
  }
189
208
  }
190
209
 
210
+ if (bodyContext.backwardProver && context.hybridStats) mergeBackwardStats(context.hybridStats, bodyContext.backwardProver.stats);
191
211
  return { applications, added };
192
212
  }
193
213
 
214
+ function prepareBodyContext(program, store, context) {
215
+ if (!context.hybridBackwardPredicates || context.hybridBackwardPredicates.size === 0) return context;
216
+ return {
217
+ ...context,
218
+ backwardProver: new BackwardProver(program, {
219
+ ...context,
220
+ store,
221
+ allowedPredicates: context.hybridBackwardPredicates,
222
+ }),
223
+ };
224
+ }
225
+
226
+ function emptyBackwardStats() {
227
+ return { mode: 'hybrid', goals: 0, facts: 0, rules: 0, memoHits: 0, memoStores: 0, maxDepth: 0 };
228
+ }
229
+
230
+ function mergeBackwardStats(total, item) {
231
+ if (!total || !item) return;
232
+ total.goals += item.goals || 0;
233
+ total.facts += item.facts || 0;
234
+ total.rules += item.rules || 0;
235
+ total.memoHits += item.memoHits || 0;
236
+ total.memoStores += item.memoStores || 0;
237
+ total.maxDepth = Math.max(total.maxDepth || 0, item.maxDepth || 0);
238
+ }
239
+
194
240
  function recursiveTermGenerationRuleIndexes(analysis) {
195
241
  const out = new Set();
196
242
  if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
@@ -299,9 +345,20 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
299
345
 
300
346
  const clause = clauses[index];
301
347
  if (clause.type === 'triple') {
348
+ const seen = new Set();
302
349
  for (const matched of store.match(clause.triple, initialBinding)) {
350
+ const key = bindingKey(matched);
351
+ seen.add(key);
303
352
  yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
304
353
  }
354
+ if (shouldUseBackwardForTriple(clause.triple, initialBinding, options)) {
355
+ for (const matched of options.backwardProver.solveTriple(clause.triple, initialBinding)) {
356
+ const key = bindingKey(matched);
357
+ if (seen.has(key)) continue;
358
+ seen.add(key);
359
+ yield* evaluateBodyStream(clauses, store, matched, options, index + 1);
360
+ }
361
+ }
305
362
  return;
306
363
  }
307
364
 
@@ -347,6 +404,12 @@ function* evaluateBodyStream(clauses, store, initialBinding = {}, options = {},
347
404
  throw new Error(`Unsupported body clause ${clause.type}`);
348
405
  }
349
406
 
407
+ function shouldUseBackwardForTriple(pattern, binding, options = {}) {
408
+ if (!options.backwardProver || !options.hybridBackwardPredicates || options.hybridBackwardPredicates.size === 0) return false;
409
+ const predicate = instantiateTerm(pattern.p, binding);
410
+ return !!(predicate && predicate.type === 'iri' && options.hybridBackwardPredicates.has(predicate.value));
411
+ }
412
+
350
413
  function bodyHasAny(clauses, store, initialBinding, options) {
351
414
  for (const _ of evaluateBodyStream(clauses, store, initialBinding, options)) return true;
352
415
  return false;
package/src/query.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const { parseQuery } = require('./parser.js');
4
4
  const { TripleStore, bindingKey } = require('./store.js');
5
5
  const { evaluateBody } = require('./engine.js');
6
+ const { backwardQuery, planBackwardQuery, preferredBackwardPredicates } = require('./backward.js');
6
7
 
7
8
  function queryResult(result, querySpec, options = {}) {
8
9
  const store = new TripleStore(result.closure || []);
@@ -13,28 +14,89 @@ function queryResult(result, querySpec, options = {}) {
13
14
  prefixes: result.prefixes,
14
15
  select,
15
16
  bindings: projectBindings(bindings, select),
17
+ mode: options.queryMode === 'hybrid' || options.hybrid ? 'hybrid' : 'forward',
16
18
  };
17
19
  }
18
20
 
21
+ function queryProgram(program, querySpec, options = {}) {
22
+ const mode = options.queryMode || 'auto';
23
+ if (mode !== 'forward' && mode !== 'hybrid') {
24
+ const planned = planBackwardQuery(program, querySpec, options);
25
+ if (planned.ok) {
26
+ const result = backwardQuery(program, querySpec, options);
27
+ if (result.ok) {
28
+ const select = normalizeSelect(querySpec.select, result.bindings);
29
+ return {
30
+ baseIRI: program.baseIRI,
31
+ prefixes: program.prefixes,
32
+ select,
33
+ bindings: projectBindings(result.bindings, select),
34
+ mode: 'backward',
35
+ stats: result.stats,
36
+ };
37
+ }
38
+ if (mode === 'backward') throw new Error(result.reason || 'Backward query failed');
39
+ } else if (mode === 'backward') {
40
+ throw new Error(`Backward query is not supported for this ruleset: ${planned.reason}`);
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
19
46
  function runQuery(source, querySource = null, options = {}) {
20
47
  const { run, compile } = require('./api.js');
21
- const { program, diagnostics } = compile(source, options);
22
- const result = run(program, options);
23
- result.diagnostics = diagnostics;
48
+ const { program, diagnostics, analysis } = compile(source, options);
24
49
 
25
50
  let querySpec;
26
51
  if (querySource) querySpec = parseQuery(querySource, { ...options, prefixes: program.prefixes, baseIRI: program.baseIRI });
27
52
  else throw new Error('No query supplied. Use --query or --query-file with a raw body pattern.');
28
53
 
29
- const query = queryResult(result, querySpec, options);
30
- return { ...result, query };
54
+ const direct = queryProgram(program, querySpec, options);
55
+ if (direct) {
56
+ return {
57
+ baseIRI: program.baseIRI,
58
+ version: program.version || null,
59
+ imports: program.imports || [],
60
+ prefixes: program.prefixes,
61
+ input: program.data.slice(),
62
+ inferred: [],
63
+ closure: program.data.slice(),
64
+ iterations: 0,
65
+ layers: [],
66
+ ruleApplications: 0,
67
+ perRule: [],
68
+ trace: [],
69
+ diagnostics,
70
+ analysis,
71
+ query: direct,
72
+ };
73
+ }
74
+
75
+ const runOptions = queryRunOptions(program, querySpec, options);
76
+ const result = run(program, runOptions);
77
+ result.diagnostics = diagnostics;
78
+ result.query = queryResult(result, querySpec, runOptions);
79
+ return result;
80
+ }
81
+
82
+ function queryRunOptions(program, querySpec, options = {}) {
83
+ if (shouldUseHybridForQuery(program, querySpec, options)) return { ...options, hybrid: true };
84
+ return options;
85
+ }
86
+
87
+ function shouldUseHybridForQuery(program, querySpec, options = {}) {
88
+ const mode = options.queryMode || 'auto';
89
+ if (options.hybrid || mode === 'hybrid') return true;
90
+ if (mode !== 'auto') return false;
91
+ if (!querySpec) return false;
92
+ return preferredBackwardPredicates(program, options).size > 0;
31
93
  }
32
94
 
33
95
  function normalizeSelect(select, bindings) {
34
96
  if (select && select.length > 0) return select.slice();
35
97
  const vars = new Set();
36
98
  for (const binding of bindings) for (const key of Object.keys(binding)) vars.add(key);
37
- return Array.from(vars).sort();
99
+ return Array.from(vars).sort().filter((name) => !name.includes('__b'));
38
100
  }
39
101
 
40
102
  function projectBindings(bindings, select) {
@@ -52,4 +114,4 @@ function projectBindings(bindings, select) {
52
114
  return out;
53
115
  }
54
116
 
55
- module.exports = { runQuery, queryResult, parseQuery, normalizeSelect, projectBindings };
117
+ module.exports = { runQuery, queryResult, queryProgram, queryRunOptions, shouldUseHybridForQuery, parseQuery, normalizeSelect, projectBindings };
package/src/store.js CHANGED
@@ -8,6 +8,7 @@ class TripleStore {
8
8
  this.byPredicate = new Map();
9
9
  this.byPredicateSubject = new Map();
10
10
  this.byPredicateObject = new Map();
11
+ this.version = 0;
11
12
  for (const triple of triples) this.add(triple);
12
13
  }
13
14
 
@@ -22,6 +23,7 @@ class TripleStore {
22
23
  addIndex(this.byPredicate, predicate, key, normalized);
23
24
  addNestedIndex(this.byPredicateSubject, predicate, subject, key, normalized);
24
25
  addNestedIndex(this.byPredicateObject, predicate, object, key, normalized);
26
+ this.version += 1;
25
27
  return true;
26
28
  }
27
29
 
package/test/api.test.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { test, main } = require('./harness.js').createHarness('API');
4
4
  const assert = require('node:assert/strict');
5
- const { parse, compile, run, runToString } = require('../src/index.js');
5
+ const { parse, compile, run, runToString, runQuery } = require('../src/index.js');
6
6
  const { tripleKey } = require('../src/term.js');
7
7
 
8
8
  test('parse reads prefixes, data, and rules', () => {
@@ -159,6 +159,114 @@ RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
159
159
  assert.match(output, /\?d = :carol/);
160
160
  });
161
161
 
162
+
163
+
164
+ test('backward query mode proves recursive derived predicates with tabling', () => {
165
+ const { runQuery, formatBindings } = require('../src/index.js');
166
+ const result = runQuery(`
167
+ PREFIX : <http://example/>
168
+ DATA { :alice :parentOf :bob . :bob :parentOf :carol . }
169
+ RULE { ?x :ancestorOf ?y } WHERE { ?x :parentOf ?y }
170
+ RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
171
+ `, ':alice :ancestorOf ?d', { queryMode: 'backward' });
172
+ assert.equal(result.iterations, 0);
173
+ assert.equal(result.query.mode, 'backward');
174
+ assert.ok(result.query.stats.memoStores > 0);
175
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
176
+ assert.match(output, /\?d = :bob/);
177
+ assert.match(output, /\?d = :carol/);
178
+ });
179
+
180
+ test('backward query mode computes function-like rules just in time', () => {
181
+ const { runQuery, formatBindings } = require('../src/index.js');
182
+ const result = runQuery(`
183
+ PREFIX : <http://example/>
184
+ DATA { :alice :score 41 . :bob :score 2 . }
185
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
186
+ `, '?who :nextScore ?score', { queryMode: 'backward' });
187
+ assert.equal(result.iterations, 0);
188
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
189
+ assert.match(output, /\?score = 42; \?who = :alice/);
190
+ assert.match(output, /\?score = 3; \?who = :bob/);
191
+ });
192
+
193
+ test('auto query mode uses backward planning for supported rules', () => {
194
+ const { runQuery } = require('../src/index.js');
195
+ const result = runQuery(`
196
+ PREFIX : <http://example/>
197
+ DATA { :a :p :b . }
198
+ RULE { ?x :q ?y } WHERE { ?x :p ?y }
199
+ `, ':a :q ?y');
200
+ assert.equal(result.query.mode, 'backward');
201
+ assert.equal(result.iterations, 0);
202
+ });
203
+
204
+
205
+
206
+ test('hybrid execution proves function-like body predicates backward without materializing them', () => {
207
+ const result = run(`
208
+ PREFIX : <http://example/>
209
+ DATA { :alice :score 41 . :bob :score 2 . }
210
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
211
+ RULE { ?x :ready true } WHERE { ?x :nextScore 42 }
212
+ `, { hybrid: true });
213
+ const keys = result.closure.map(tripleKey).join('\n');
214
+ assert.match(keys, /ready/);
215
+ assert.doesNotMatch(keys, /nextScore/);
216
+ assert.equal(result.perRule[0].applications, 0);
217
+ assert.equal(result.perRule[0].backward, true);
218
+ assert.ok(result.hybridStats.goals > 0);
219
+ assert.ok(result.hybridStats.rules > 0);
220
+ });
221
+
222
+ test('hybrid query mode runs forward rules with backward body calls', () => {
223
+ const { runQuery, formatBindings } = require('../src/index.js');
224
+ const result = runQuery(`
225
+ PREFIX : <http://example/>
226
+ DATA { :alice :score 41 . :bob :score 2 . }
227
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
228
+ RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
229
+ `, '?who :ready ?score', { queryMode: 'hybrid' });
230
+ assert.equal(result.query.mode, 'hybrid');
231
+ assert.ok(result.hybridStats.goals > 0);
232
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
233
+ assert.match(output, /\?score = 42; \?who = :alice/);
234
+ assert.doesNotMatch(output, /bob/);
235
+ });
236
+
237
+ test('auto query mode uses pure backward planning when unsupported rules are irrelevant', () => {
238
+ const { runQuery, formatBindings } = require('../src/index.js');
239
+ const result = runQuery(`
240
+ PREFIX : <http://example/>
241
+ DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
242
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
243
+ RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
244
+ RULE { [] :unrelated :x } WHERE { :seed :seen :x }
245
+ `, '?who :ready ?score');
246
+ assert.equal(result.query.mode, 'backward');
247
+ assert.ok(result.query.stats.goals > 0);
248
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
249
+ assert.match(output, /\?score = 42; \?who = :alice/);
250
+ assert.doesNotMatch(output, /bob/);
251
+ });
252
+
253
+ test('auto query mode falls back to hybrid when a demanded predicate has unsupported rules', () => {
254
+ const { runQuery, formatBindings } = require('../src/index.js');
255
+ const result = runQuery(`
256
+ PREFIX : <http://example/>
257
+ DATA { :alice :score 41 . :bob :score 2 . :seed :seen :x . }
258
+ RULE { ?x :nextScore ?m } WHERE { ?x :score ?n . SET(?m := ?n + 1) }
259
+ RULE { ?x :ready ?score } WHERE { ?x :nextScore ?score . FILTER(?score = 42) }
260
+ RULE { [] :ready 999 } WHERE { :seed :seen :x }
261
+ `, '?who :ready ?score');
262
+ assert.equal(result.query.mode, 'hybrid');
263
+ assert.ok(result.hybridStats.goals > 0);
264
+ assert.equal(result.perRule[0].backward, true);
265
+ assert.doesNotMatch(result.closure.map(tripleKey).join('\n'), /nextScore/);
266
+ const output = formatBindings(result.query.bindings, result.prefixes, result.query.select);
267
+ assert.match(output, /\?score = 42; \?who = :alice/);
268
+ });
269
+
162
270
  test('parseQuery accepts raw body text and rejects non-SRL QUERY/SELECT syntax', () => {
163
271
  const { parseQuery } = require('../src/index.js');
164
272
  const raw = parseQuery('?x :p ?y', { prefixes: { '': 'http://example/' } });
@@ -599,4 +707,22 @@ RULE { ?x :p ?v1 } WHERE { ?x :p ?v . SET(?v1 := ?v + 1) }
599
707
  `, { shacl12Conformance: true, strict: true }), /creates terms in a recursive dependency cycle/);
600
708
  });
601
709
 
710
+
711
+ test('auto backward query ignores unsupported rules outside the demanded slice', () => {
712
+ const source = `
713
+ PREFIX : <http://example/>
714
+ DATA { :alice :parent :bob . :bob :parent :carol . :a :q :b . :b :q :c . }
715
+ RULE { ?x :ancestor ?y } WHERE { ?x :parent ?y }
716
+ RULE { ?x :ancestor ?z } WHERE { ?x :parent ?y . ?y :ancestor ?z }
717
+ RULE { ?x :twoStep ?y } WHERE { ?x :q/:q ?y }
718
+ `;
719
+ const result = runQuery(source, ':alice :ancestor ?who');
720
+ assert.equal(result.query.mode, 'backward');
721
+ assert.ok(result.query.stats.goals > 0);
722
+ assert.deepEqual(
723
+ result.query.bindings.map((binding) => binding.who.value).sort(),
724
+ ['http://example/bob', 'http://example/carol'],
725
+ );
726
+ });
727
+
602
728
  main();
package/test/cli.test.js CHANGED
@@ -22,7 +22,7 @@ function readmeCliOptions() {
22
22
  test('CLI help documents RDF Message Log flags', () => {
23
23
  const text = help();
24
24
  assert.match(text, /--rdf-messages\s+Parse input as an RDF Message Log/);
25
- assert.match(text, /--stream-messages\s+Replay RDF Message Log envelopes/);
25
+ assert.doesNotMatch(text, /--stream-messages/);
26
26
  assert.match(text, /--include-message-facts\s+Include payload facts while parsing RDF Message Logs/);
27
27
  });
28
28
 
@@ -34,8 +34,17 @@ test('README CLI options stay in sync with --help', () => {
34
34
 
35
35
  test('RDF Message Log flags are accepted by parseArgs', () => {
36
36
  assert.equal(parseArgs(['--rdf-messages']).options.rdfMessages, true);
37
- assert.equal(parseArgs(['--stream-messages']).options.rdfMessages, true);
38
37
  assert.equal(parseArgs(['--include-message-facts']).options.includeMessageFacts, true);
39
38
  });
40
39
 
40
+ test('query mode flag is accepted by parseArgs', () => {
41
+ assert.equal(parseArgs(['--query-mode', 'auto']).options.queryMode, 'auto');
42
+ assert.equal(parseArgs(['--query-mode', 'forward']).options.queryMode, 'forward');
43
+ assert.equal(parseArgs(['--query-mode', 'backward']).options.queryMode, 'backward');
44
+ assert.equal(parseArgs(['--hybrid']).options.hybrid, true);
45
+ assert.throws(() => parseArgs(['--query-mode', 'hybrid']), /--query-mode requires auto, forward, or backward/);
46
+ assert.throws(() => parseArgs(['--query-mode', 'sideways']), /--query-mode requires auto, forward, or backward/);
47
+ assert.throws(() => parseArgs(['--stream-messages']), /Unknown option --stream-messages/);
48
+ });
49
+
41
50
  main();