eyeleng 1.0.11 → 1.0.13

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/eyeleng.js CHANGED
@@ -40,7 +40,7 @@
40
40
  const VERSION = readPackageVersion();
41
41
 
42
42
  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\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 --stream-messages Replay RDF Message Log envelopes\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`;
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 --stream-messages Replay RDF Message Log envelopes\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
44
  }
45
45
 
46
46
  function parseArgs(argv) {
@@ -148,7 +148,7 @@
148
148
  for (const edge of edges) {
149
149
  const from = formatRuleName(analysis.dependency.rules[edge.from].name, prefixes);
150
150
  const to = formatRuleName(analysis.dependency.rules[edge.to].name, prefixes);
151
- const kind = edge.negative ? 'NOT' : 'uses';
151
+ const kind = edge.negated ? 'NOT' : (edge.termGeneration ? 'generates' : 'uses');
152
152
  stderr.write(`eyeleng: deps: ${from} --${kind} ${edge.predicate ? compactIRI(edge.predicate, prefixes) : '*'}--> ${to}\n`);
153
153
  }
154
154
  if (analysis.dependency.layers && analysis.dependency.layers.length > 0) {
@@ -4278,8 +4278,8 @@
4278
4278
  "src/engine.js": function (require, module, exports) {
4279
4279
  'use strict';
4280
4280
 
4281
- const { TripleStore, bindingKey, instantiateTriple } = require('./store.js');
4282
- const { tripleKey, termEquals } = require('./term.js');
4281
+ const { TripleStore, bindingKey } = require('./store.js');
4282
+ const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
4283
4283
  const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
4284
4284
  const { analyze } = require('./analyze.js');
4285
4285
 
@@ -4299,7 +4299,7 @@
4299
4299
  runOnce: !!rule.runOnce,
4300
4300
  }));
4301
4301
 
4302
- const analysis = options.analysis || analyze(program);
4302
+ const analysis = options.analysis || analyze(program, options);
4303
4303
  if (analysis.errors && analysis.errors.length > 0 && !options.ignoreAnalysisErrors) {
4304
4304
  throw new Error(`Analysis failed: ${analysis.errors.map((error) => error.message).join('; ')}`);
4305
4305
  }
@@ -4310,6 +4310,9 @@
4310
4310
  layerIndexes,
4311
4311
  analysis.dependency ? analysis.dependency.edges : [],
4312
4312
  );
4313
+ const relaxedRecursiveRunOnce = options.relaxedRecursion === false
4314
+ ? new Set()
4315
+ : recursiveTermGenerationRuleIndexes(analysis);
4313
4316
  const baseContext = {
4314
4317
  ...evalOptions,
4315
4318
  maxIterations,
@@ -4325,8 +4328,8 @@
4325
4328
 
4326
4329
  for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
4327
4330
  const layer = layerIndexes[layerIndex];
4328
- const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce);
4329
- const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce);
4331
+ const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
4332
+ const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
4330
4333
 
4331
4334
  if (runOnce.length > 0) {
4332
4335
  iterations += 1;
@@ -4426,6 +4429,7 @@
4426
4429
  let added = 0;
4427
4430
  const dedupeBindings = rule.body.some((clause) => clause.type === 'path');
4428
4431
  const seenBindings = dedupeBindings ? new Set() : null;
4432
+ const headBlankLabels = collectHeadBlankLabels(rule.head);
4429
4433
 
4430
4434
  const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
4431
4435
  ? store.match(rule.body[0].triple, {})
@@ -4440,8 +4444,10 @@
4440
4444
  applications += 1;
4441
4445
  context.perRule[ruleIndex].applications += 1;
4442
4446
 
4447
+ const headBlankMap = headBlankLabels.size > 0 ? new Map() : null;
4448
+ const skolemKey = headBlankMap ? skolemizationKey(ruleIndex, binding) : null;
4443
4449
  for (const head of rule.head) {
4444
- const triple = instantiateTriple(head, binding);
4450
+ const triple = instantiateHeadTriple(head, binding, headBlankLabels, headBlankMap, skolemKey);
4445
4451
  if (!triple) continue;
4446
4452
  if (store.add(triple)) {
4447
4453
  added += 1;
@@ -4463,6 +4469,94 @@
4463
4469
  return { applications, added };
4464
4470
  }
4465
4471
 
4472
+ function recursiveTermGenerationRuleIndexes(analysis) {
4473
+ const out = new Set();
4474
+ if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
4475
+ const byName = new Map((analysis.dependency.rules || []).map((rule) => [rule.name, rule.index]));
4476
+ for (const diagnostic of analysis.diagnostics) {
4477
+ if (diagnostic.code !== 'recursive-assignment-rule') continue;
4478
+ if (byName.has(diagnostic.rule)) out.add(byName.get(diagnostic.rule));
4479
+ }
4480
+ return out;
4481
+ }
4482
+
4483
+ function instantiateHeadTriple(pattern, binding, headBlankLabels, headBlankMap, skolemKey) {
4484
+ const s = instantiateHeadTerm(pattern.s, binding, headBlankLabels, headBlankMap, skolemKey);
4485
+ const p = instantiateHeadTerm(pattern.p, binding, headBlankLabels, headBlankMap, skolemKey);
4486
+ const o = instantiateHeadTerm(pattern.o, binding, headBlankLabels, headBlankMap, skolemKey);
4487
+ if (!s || !p || !o) return null;
4488
+ if (p.type !== 'iri') return null;
4489
+ return { s, p, o };
4490
+ }
4491
+
4492
+ function instantiateHeadTerm(term, binding, headBlankLabels, headBlankMap, skolemKey) {
4493
+ if (term.type === 'var') return binding[term.value] || null;
4494
+ if (term.type === 'blank' && headBlankLabels.has(term.value)) {
4495
+ let label = headBlankMap.get(term.value);
4496
+ if (!label) {
4497
+ label = `sk_${deterministicSkolemIdFromKey(`${skolemKey}|${term.value}`).replace(/-/g, '_')}`;
4498
+ headBlankMap.set(term.value, label);
4499
+ }
4500
+ return blankNode(label);
4501
+ }
4502
+ if (term.type === 'triple') {
4503
+ const s = instantiateHeadTerm(term.s, binding, headBlankLabels, headBlankMap, skolemKey);
4504
+ const p = instantiateHeadTerm(term.p, binding, headBlankLabels, headBlankMap, skolemKey);
4505
+ const o = instantiateHeadTerm(term.o, binding, headBlankLabels, headBlankMap, skolemKey);
4506
+ if (!s || !p || !o) return null;
4507
+ return tripleTerm(s, p, o);
4508
+ }
4509
+ return term;
4510
+ }
4511
+
4512
+ function collectHeadBlankLabels(head) {
4513
+ const labels = new Set();
4514
+ for (const triple of head || []) {
4515
+ collectBlankLabelsFromTerm(triple.s, labels);
4516
+ collectBlankLabelsFromTerm(triple.p, labels);
4517
+ collectBlankLabelsFromTerm(triple.o, labels);
4518
+ }
4519
+ return labels;
4520
+ }
4521
+
4522
+ function collectBlankLabelsFromTerm(term, labels) {
4523
+ if (!term) return;
4524
+ if (term.type === 'blank') {
4525
+ labels.add(term.value);
4526
+ return;
4527
+ }
4528
+ if (term.type === 'triple') {
4529
+ collectBlankLabelsFromTerm(term.s, labels);
4530
+ collectBlankLabelsFromTerm(term.p, labels);
4531
+ collectBlankLabelsFromTerm(term.o, labels);
4532
+ }
4533
+ }
4534
+
4535
+ function skolemizationKey(ruleIndex, binding) {
4536
+ let out = `rule:${ruleIndex}`;
4537
+ for (const name of Object.keys(binding).sort()) {
4538
+ const value = binding[name];
4539
+ out += `|${name}=${value ? termKey(value) : 'unbound'}`;
4540
+ }
4541
+ return out;
4542
+ }
4543
+
4544
+ function deterministicSkolemIdFromKey(key) {
4545
+ let h1 = 0x811c9dc5;
4546
+ let h2 = 0x811c9dc5;
4547
+ let h3 = 0x811c9dc5;
4548
+ let h4 = 0x811c9dc5;
4549
+ for (let i = 0; i < key.length; i += 1) {
4550
+ const c = key.charCodeAt(i);
4551
+ h1 ^= c; h1 = Math.imul(h1, 0x01000193) >>> 0;
4552
+ h2 ^= c + 1; h2 = Math.imul(h2, 0x01000193) >>> 0;
4553
+ h3 ^= c + 2; h3 = Math.imul(h3, 0x01000193) >>> 0;
4554
+ h4 ^= c + 3; h4 = Math.imul(h4, 0x01000193) >>> 0;
4555
+ }
4556
+ return [h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, '0')).join('');
4557
+ }
4558
+
4559
+
4466
4560
  function evaluateBody(clauses, store, initialBinding = {}, options = {}) {
4467
4561
  const bindings = [];
4468
4562
  const seen = new Set();
@@ -4819,8 +4913,8 @@
4819
4913
  function analyze(program, options = {}) {
4820
4914
  const diagnostics = [];
4821
4915
  const dependency = dependencyGraph(program, options);
4822
- const hasRunOnceRules = program.rules.some((rule) => rule.runOnce);
4823
- const recursiveIndexes = hasRunOnceRules ? recursiveRuleIndexes(dependency) : new Set();
4916
+ const hasTermGeneratingRules = dependency.rules.some((rule) => rule.createsTerms);
4917
+ const recursiveIndexes = hasTermGeneratingRules ? recursiveRuleIndexes(dependency) : new Set();
4824
4918
 
4825
4919
  program.rules.forEach((rule, index) => {
4826
4920
  const name = ruleName(rule, index);
@@ -4852,12 +4946,13 @@
4852
4946
 
4853
4947
  diagnostics.push(...sequentialWellFormednessDiagnostics(rule.body, name, program.prefixes || {}));
4854
4948
 
4855
- if (rule.runOnce && recursiveIndexes.has(index)) {
4949
+ const depRule = dependency.rules[index] || {};
4950
+ if (depRule.createsTerms && recursiveIndexes.has(index)) {
4856
4951
  diagnostics.push({
4857
4952
  code: 'recursive-assignment-rule',
4858
4953
  severity: 'warning',
4859
4954
  rule: name,
4860
- message: `${displayRuleName(name, program.prefixes || {})} is a run-once rule in a recursive dependency cycle`,
4955
+ message: `${displayRuleName(name, program.prefixes || {})} creates terms in a recursive dependency cycle; relaxed mode allows this but termination is not guaranteed (use --strict to reject it)`,
4861
4956
  });
4862
4957
  }
4863
4958
 
@@ -4904,34 +4999,47 @@
4904
4999
  negativePredicates: new Set(negativePatterns.flatMap((triple) => predicateIRIs(triple))),
4905
5000
  runOnce: !!rule.runOnce,
4906
5001
  hasAssignment: ruleHasAssignment(rule, options),
5002
+ hasTermGeneratingAssignment: ruleHasTermGeneratingAssignment(rule, options),
4907
5003
  headHasBlankNode: ruleHeadHasBlankNode(rule),
5004
+ createsTerms: ruleCreatesTerms(rule, options),
4908
5005
  };
4909
5006
  });
4910
5007
 
4911
5008
  const edgeMap = new Map();
4912
- function addEdge(from, to, negative, predicate) {
5009
+ function addEdge(from, to, kind, predicate) {
4913
5010
  const label = predicate || '*';
4914
5011
  const key = `${from.index}->${to.index}:${label}`;
5012
+ const negated = kind === 'negated';
5013
+ const termGeneration = kind === 'term-generation';
4915
5014
  const existing = edgeMap.get(key);
4916
5015
  if (existing) {
4917
- existing.negative = existing.negative || negative;
5016
+ existing.negated = existing.negated || negated;
5017
+ existing.termGeneration = existing.termGeneration || termGeneration;
5018
+ existing.negative = existing.negated || existing.termGeneration;
4918
5019
  return;
4919
5020
  }
4920
- edgeMap.set(key, { from: from.index, to: to.index, negative, predicate });
5021
+ edgeMap.set(key, {
5022
+ from: from.index,
5023
+ to: to.index,
5024
+ negative: negated || termGeneration,
5025
+ negated,
5026
+ termGeneration,
5027
+ predicate,
5028
+ });
4921
5029
  }
4922
5030
 
4923
5031
  const headIndex = buildHeadTemplateIndex(rules);
4924
5032
 
4925
5033
  for (const from of rules) {
4926
- const forceClosed = from.hasAssignment || from.headHasBlankNode;
5034
+ const forceClosed = from.createsTerms;
4927
5035
  for (const pattern of from.positivePatterns) {
4928
5036
  for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
4929
- if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed, dependencyPredicateLabel(pattern));
5037
+ if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed ? 'term-generation' : 'positive', dependencyPredicateLabel(pattern));
4930
5038
  }
4931
5039
  }
4932
5040
  for (const pattern of from.negativePatterns) {
4933
5041
  for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
4934
- if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], true, dependencyPredicateLabel(pattern));
5042
+ if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], 'negated', dependencyPredicateLabel(pattern));
4935
5043
  }
4936
5044
  }
4937
5045
  }
@@ -4947,7 +5055,7 @@
4947
5055
  const unstratifiedCycles = [];
4948
5056
  const seen = new Set();
4949
5057
  for (const edge of edges) {
4950
- if (!edge.negative) continue;
5058
+ if (!edge.negated) continue;
4951
5059
  if (edge.from === edge.to && rules[edge.from].runOnce && !rules[edge.from].headHasBlankNode) continue;
4952
5060
  if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
4953
5061
  const component = components[componentOf.get(edge.from)];
@@ -4970,7 +5078,10 @@
4970
5078
  positivePredicates: Array.from(rule.positivePredicates),
4971
5079
  negativePredicates: Array.from(rule.negativePredicates),
4972
5080
  runOnce: rule.runOnce,
5081
+ hasAssignment: rule.hasAssignment,
5082
+ hasTermGeneratingAssignment: rule.hasTermGeneratingAssignment,
4973
5083
  headHasBlankNode: rule.headHasBlankNode,
5084
+ createsTerms: rule.createsTerms,
4974
5085
  })),
4975
5086
  edges,
4976
5087
  components: components.map((component) => component.map((ruleIndex) => rules[ruleIndex].name)),
@@ -5140,7 +5251,14 @@
5140
5251
  function recursiveRuleIndexes(dependency) {
5141
5252
  const out = new Set();
5142
5253
  const ruleByName = new Map(dependency.rules.map((rule) => [rule.name, rule]));
5143
- const ruleByIndex = new Map(dependency.rules.map((rule) => [rule.index, rule]));
5254
+ const componentOf = new Map();
5255
+
5256
+ dependency.components.forEach((component, componentIndex) => {
5257
+ for (const name of component) {
5258
+ const rule = ruleByName.get(name);
5259
+ if (rule) componentOf.set(rule.index, componentIndex);
5260
+ }
5261
+ });
5144
5262
 
5145
5263
  for (const component of dependency.components) {
5146
5264
  if (component.length <= 1) continue;
@@ -5151,9 +5269,9 @@
5151
5269
  }
5152
5270
 
5153
5271
  for (const edge of dependency.edges) {
5154
- const rule = ruleByIndex.get(edge.from);
5155
- if (edge.from === edge.to && edge.negative && rule && rule.runOnce && !rule.headHasBlankNode) continue;
5156
- out.add(edge.from);
5272
+ const rule = (dependency.rules || []).find((candidate) => candidate.index === edge.from);
5273
+ if (edge.from === edge.to && edge.negated && rule && rule.runOnce && !rule.headHasBlankNode) continue;
5274
+ if (edge.from === edge.to || componentOf.get(edge.from) === componentOf.get(edge.to)) out.add(edge.from);
5157
5275
  }
5158
5276
  return out;
5159
5277
  }
@@ -5426,7 +5544,26 @@
5426
5544
  }
5427
5545
 
5428
5546
  function ruleHasAssignment(rule, options = {}) {
5429
- return !!options.shacl12Conformance && (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
5547
+ return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
5548
+ }
5549
+
5550
+ function ruleHasTermGeneratingAssignment(rule, options = {}) {
5551
+ return (rule.body || []).some((clause) => (clause.type === 'set' || clause.type === 'bind') && assignmentMayCreateNewTerm(clause.expr));
5552
+ }
5553
+
5554
+ function ruleCreatesTerms(rule, options = {}) {
5555
+ return ruleHeadHasBlankNode(rule) || ruleHasTermGeneratingAssignment(rule, options);
5556
+ }
5557
+
5558
+ function assignmentMayCreateNewTerm(expr) {
5559
+ // Simple aliases and constants are not term generators: they select from a
5560
+ // fixed term or from already-bound terms. Expressions that compute values
5561
+ // through operators or functions may create an unbounded sequence when used
5562
+ // recursively, e.g. SET(?v1 := ?v + 1), so they remain part of the strict
5563
+ // recursive-new-terms check.
5564
+ if (!expr) return false;
5565
+ if (expr.type === 'var' || expr.type === 'term' || expr.type === 'literal') return false;
5566
+ return true;
5430
5567
  }
5431
5568
 
5432
5569
  function ruleHeadHasBlankNode(rule) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeleng",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "The EYE Logic Engine: a JavaScript implementation of SHACL Rules, including SRL and RDF Rules syntax front-ends.",
5
5
  "keywords": [
6
6
  "SRL",
@@ -36,16 +36,17 @@
36
36
  "build": "node tools/bundle.js",
37
37
  "pretest": "node tools/bundle.js",
38
38
  "test": "node test/run.js",
39
+ "test:api": "node test/api.test.js",
40
+ "test:examples": "node test/examples.test.js",
39
41
  "preversion": "npm test",
40
42
  "version": "node tools/bundle.js",
41
43
  "postversion": "git push origin HEAD --follow-tags",
42
44
  "w3c:rules": "node tools/w3c-shacl12-rules.js",
45
+ "w3c:rules:json": "node tools/w3c-shacl12-rules.js --json",
46
+ "w3c:rules:earl": "node tools/w3c-shacl12-rules.js --earl",
43
47
  "w3c:rdf": "node tools/w3c-rdf.js",
44
48
  "w3c:rdf:json": "node tools/w3c-rdf.js --json",
45
49
  "w3c:rdf:earl": "node tools/w3c-rdf.js --earl",
46
- "w3c:all": "npm run w3c:rules && npm run w3c:rdf",
47
- "w3c:rules:json": "node tools/w3c-shacl12-rules.js --json",
48
- "w3c:rules:earl": "node tools/w3c-shacl12-rules.js --earl",
49
50
  "bench": "node tools/perf-bench.js --check",
50
51
  "bench:report": "node tools/perf-bench.js --report",
51
52
  "bench:update": "node tools/perf-bench.js --update"