eyeleng 1.0.11 → 1.0.12
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 +4 -2
- package/dist/browser/eyeleng.browser.js +159 -22
- package/examples/assignment.srl +1 -1
- package/examples/base-and-literals.srl +1 -1
- package/examples/output/cat-koko.trig +2 -2
- package/examples/path-discovery.srl +5 -5
- package/eyeleng.js +161 -24
- package/package.json +1 -1
- package/src/analyze.js +59 -16
- package/src/cli.js +2 -2
- package/src/engine.js +100 -6
- package/src/shacl12RulesManifest.js +5 -0
- package/test/api.test.js +82 -1
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ then the body matches with `?parent = :alice` and `?child = :bob`, and the head
|
|
|
76
76
|
|
|
77
77
|
Negation is handled by stratified evaluation: rules are grouped into dependency layers, and recursion through negation is rejected so the result stays deterministic.
|
|
78
78
|
|
|
79
|
+
Recursive rules that create new terms through `SET`/`BIND` or head blank nodes run in relaxed mode by default. Relaxed mode can derive useful finite closures, but termination is not guaranteed; use `--max-iterations` as a safety valve. `--strict` rejects these recursive term-generating cycles at analysis time. Head blank nodes are deterministically skolemized per rule and solution mapping, so the same firing reuses the same witness node instead of creating a fresh one each pass.
|
|
80
|
+
|
|
79
81
|
## Language surface
|
|
80
82
|
|
|
81
83
|
SRL supports the practical rule features used by the SHACL 1.2 Rules tests and examples:
|
|
@@ -90,7 +92,7 @@ DATA {
|
|
|
90
92
|
|
|
91
93
|
RULE { ?x :grade ?grade } WHERE {
|
|
92
94
|
?x :score ?score .
|
|
93
|
-
FILTER(?score >= 5)
|
|
95
|
+
FILTER(?score >= 5)
|
|
94
96
|
BIND(concat("pass-", str(?score)) AS ?grade)
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -202,7 +204,7 @@ Important options:
|
|
|
202
204
|
--trace print derivation trace to stderr, or include it in JSON
|
|
203
205
|
--stats print iteration and triple counts to stderr
|
|
204
206
|
--check parse and analyze only; do not run rules
|
|
205
|
-
--strict treat static warnings as errors
|
|
207
|
+
--strict treat static warnings as errors, including recursive term generation
|
|
206
208
|
--deps print rule dependency edges during --check
|
|
207
209
|
--query TEXT run a raw SRL body pattern over the closure
|
|
208
210
|
--query-file FILE read a raw SRL body pattern from a file
|
|
@@ -4048,8 +4048,8 @@
|
|
|
4048
4048
|
"src/engine.js": function (require, module, exports) {
|
|
4049
4049
|
'use strict';
|
|
4050
4050
|
|
|
4051
|
-
const { TripleStore, bindingKey
|
|
4052
|
-
const { tripleKey, termEquals } = require('./term.js');
|
|
4051
|
+
const { TripleStore, bindingKey } = require('./store.js');
|
|
4052
|
+
const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
|
|
4053
4053
|
const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
|
|
4054
4054
|
const { analyze } = require('./analyze.js');
|
|
4055
4055
|
|
|
@@ -4069,7 +4069,7 @@
|
|
|
4069
4069
|
runOnce: !!rule.runOnce,
|
|
4070
4070
|
}));
|
|
4071
4071
|
|
|
4072
|
-
const analysis = options.analysis || analyze(program);
|
|
4072
|
+
const analysis = options.analysis || analyze(program, options);
|
|
4073
4073
|
if (analysis.errors && analysis.errors.length > 0 && !options.ignoreAnalysisErrors) {
|
|
4074
4074
|
throw new Error(`Analysis failed: ${analysis.errors.map((error) => error.message).join('; ')}`);
|
|
4075
4075
|
}
|
|
@@ -4080,6 +4080,9 @@
|
|
|
4080
4080
|
layerIndexes,
|
|
4081
4081
|
analysis.dependency ? analysis.dependency.edges : [],
|
|
4082
4082
|
);
|
|
4083
|
+
const relaxedRecursiveRunOnce = options.relaxedRecursion === false
|
|
4084
|
+
? new Set()
|
|
4085
|
+
: recursiveTermGenerationRuleIndexes(analysis);
|
|
4083
4086
|
const baseContext = {
|
|
4084
4087
|
...evalOptions,
|
|
4085
4088
|
maxIterations,
|
|
@@ -4095,8 +4098,8 @@
|
|
|
4095
4098
|
|
|
4096
4099
|
for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
|
|
4097
4100
|
const layer = layerIndexes[layerIndex];
|
|
4098
|
-
const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce);
|
|
4099
|
-
const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce);
|
|
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));
|
|
4100
4103
|
|
|
4101
4104
|
if (runOnce.length > 0) {
|
|
4102
4105
|
iterations += 1;
|
|
@@ -4196,6 +4199,7 @@
|
|
|
4196
4199
|
let added = 0;
|
|
4197
4200
|
const dedupeBindings = rule.body.some((clause) => clause.type === 'path');
|
|
4198
4201
|
const seenBindings = dedupeBindings ? new Set() : null;
|
|
4202
|
+
const headBlankLabels = collectHeadBlankLabels(rule.head);
|
|
4199
4203
|
|
|
4200
4204
|
const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
|
|
4201
4205
|
? store.match(rule.body[0].triple, {})
|
|
@@ -4210,8 +4214,10 @@
|
|
|
4210
4214
|
applications += 1;
|
|
4211
4215
|
context.perRule[ruleIndex].applications += 1;
|
|
4212
4216
|
|
|
4217
|
+
const headBlankMap = headBlankLabels.size > 0 ? new Map() : null;
|
|
4218
|
+
const skolemKey = headBlankMap ? skolemizationKey(ruleIndex, binding) : null;
|
|
4213
4219
|
for (const head of rule.head) {
|
|
4214
|
-
const triple =
|
|
4220
|
+
const triple = instantiateHeadTriple(head, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4215
4221
|
if (!triple) continue;
|
|
4216
4222
|
if (store.add(triple)) {
|
|
4217
4223
|
added += 1;
|
|
@@ -4233,6 +4239,94 @@
|
|
|
4233
4239
|
return { applications, added };
|
|
4234
4240
|
}
|
|
4235
4241
|
|
|
4242
|
+
function recursiveTermGenerationRuleIndexes(analysis) {
|
|
4243
|
+
const out = new Set();
|
|
4244
|
+
if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
|
|
4245
|
+
const byName = new Map((analysis.dependency.rules || []).map((rule) => [rule.name, rule.index]));
|
|
4246
|
+
for (const diagnostic of analysis.diagnostics) {
|
|
4247
|
+
if (diagnostic.code !== 'recursive-assignment-rule') continue;
|
|
4248
|
+
if (byName.has(diagnostic.rule)) out.add(byName.get(diagnostic.rule));
|
|
4249
|
+
}
|
|
4250
|
+
return out;
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
function instantiateHeadTriple(pattern, binding, headBlankLabels, headBlankMap, skolemKey) {
|
|
4254
|
+
const s = instantiateHeadTerm(pattern.s, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4255
|
+
const p = instantiateHeadTerm(pattern.p, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4256
|
+
const o = instantiateHeadTerm(pattern.o, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4257
|
+
if (!s || !p || !o) return null;
|
|
4258
|
+
if (p.type !== 'iri') return null;
|
|
4259
|
+
return { s, p, o };
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
function instantiateHeadTerm(term, binding, headBlankLabels, headBlankMap, skolemKey) {
|
|
4263
|
+
if (term.type === 'var') return binding[term.value] || null;
|
|
4264
|
+
if (term.type === 'blank' && headBlankLabels.has(term.value)) {
|
|
4265
|
+
let label = headBlankMap.get(term.value);
|
|
4266
|
+
if (!label) {
|
|
4267
|
+
label = `sk_${deterministicSkolemIdFromKey(`${skolemKey}|${term.value}`).replace(/-/g, '_')}`;
|
|
4268
|
+
headBlankMap.set(term.value, label);
|
|
4269
|
+
}
|
|
4270
|
+
return blankNode(label);
|
|
4271
|
+
}
|
|
4272
|
+
if (term.type === 'triple') {
|
|
4273
|
+
const s = instantiateHeadTerm(term.s, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4274
|
+
const p = instantiateHeadTerm(term.p, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4275
|
+
const o = instantiateHeadTerm(term.o, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
4276
|
+
if (!s || !p || !o) return null;
|
|
4277
|
+
return tripleTerm(s, p, o);
|
|
4278
|
+
}
|
|
4279
|
+
return term;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
function collectHeadBlankLabels(head) {
|
|
4283
|
+
const labels = new Set();
|
|
4284
|
+
for (const triple of head || []) {
|
|
4285
|
+
collectBlankLabelsFromTerm(triple.s, labels);
|
|
4286
|
+
collectBlankLabelsFromTerm(triple.p, labels);
|
|
4287
|
+
collectBlankLabelsFromTerm(triple.o, labels);
|
|
4288
|
+
}
|
|
4289
|
+
return labels;
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
function collectBlankLabelsFromTerm(term, labels) {
|
|
4293
|
+
if (!term) return;
|
|
4294
|
+
if (term.type === 'blank') {
|
|
4295
|
+
labels.add(term.value);
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
if (term.type === 'triple') {
|
|
4299
|
+
collectBlankLabelsFromTerm(term.s, labels);
|
|
4300
|
+
collectBlankLabelsFromTerm(term.p, labels);
|
|
4301
|
+
collectBlankLabelsFromTerm(term.o, labels);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
function skolemizationKey(ruleIndex, binding) {
|
|
4306
|
+
let out = `rule:${ruleIndex}`;
|
|
4307
|
+
for (const name of Object.keys(binding).sort()) {
|
|
4308
|
+
const value = binding[name];
|
|
4309
|
+
out += `|${name}=${value ? termKey(value) : 'unbound'}`;
|
|
4310
|
+
}
|
|
4311
|
+
return out;
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
function deterministicSkolemIdFromKey(key) {
|
|
4315
|
+
let h1 = 0x811c9dc5;
|
|
4316
|
+
let h2 = 0x811c9dc5;
|
|
4317
|
+
let h3 = 0x811c9dc5;
|
|
4318
|
+
let h4 = 0x811c9dc5;
|
|
4319
|
+
for (let i = 0; i < key.length; i += 1) {
|
|
4320
|
+
const c = key.charCodeAt(i);
|
|
4321
|
+
h1 ^= c; h1 = Math.imul(h1, 0x01000193) >>> 0;
|
|
4322
|
+
h2 ^= c + 1; h2 = Math.imul(h2, 0x01000193) >>> 0;
|
|
4323
|
+
h3 ^= c + 2; h3 = Math.imul(h3, 0x01000193) >>> 0;
|
|
4324
|
+
h4 ^= c + 3; h4 = Math.imul(h4, 0x01000193) >>> 0;
|
|
4325
|
+
}
|
|
4326
|
+
return [h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, '0')).join('');
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4329
|
+
|
|
4236
4330
|
function evaluateBody(clauses, store, initialBinding = {}, options = {}) {
|
|
4237
4331
|
const bindings = [];
|
|
4238
4332
|
const seen = new Set();
|
|
@@ -4589,8 +4683,8 @@
|
|
|
4589
4683
|
function analyze(program, options = {}) {
|
|
4590
4684
|
const diagnostics = [];
|
|
4591
4685
|
const dependency = dependencyGraph(program, options);
|
|
4592
|
-
const
|
|
4593
|
-
const recursiveIndexes =
|
|
4686
|
+
const hasTermGeneratingRules = dependency.rules.some((rule) => rule.createsTerms);
|
|
4687
|
+
const recursiveIndexes = hasTermGeneratingRules ? recursiveRuleIndexes(dependency) : new Set();
|
|
4594
4688
|
|
|
4595
4689
|
program.rules.forEach((rule, index) => {
|
|
4596
4690
|
const name = ruleName(rule, index);
|
|
@@ -4622,12 +4716,13 @@
|
|
|
4622
4716
|
|
|
4623
4717
|
diagnostics.push(...sequentialWellFormednessDiagnostics(rule.body, name, program.prefixes || {}));
|
|
4624
4718
|
|
|
4625
|
-
|
|
4719
|
+
const depRule = dependency.rules[index] || {};
|
|
4720
|
+
if (depRule.createsTerms && recursiveIndexes.has(index)) {
|
|
4626
4721
|
diagnostics.push({
|
|
4627
4722
|
code: 'recursive-assignment-rule',
|
|
4628
4723
|
severity: 'warning',
|
|
4629
4724
|
rule: name,
|
|
4630
|
-
message: `${displayRuleName(name, program.prefixes || {})}
|
|
4725
|
+
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)`,
|
|
4631
4726
|
});
|
|
4632
4727
|
}
|
|
4633
4728
|
|
|
@@ -4674,34 +4769,47 @@
|
|
|
4674
4769
|
negativePredicates: new Set(negativePatterns.flatMap((triple) => predicateIRIs(triple))),
|
|
4675
4770
|
runOnce: !!rule.runOnce,
|
|
4676
4771
|
hasAssignment: ruleHasAssignment(rule, options),
|
|
4772
|
+
hasTermGeneratingAssignment: ruleHasTermGeneratingAssignment(rule, options),
|
|
4677
4773
|
headHasBlankNode: ruleHeadHasBlankNode(rule),
|
|
4774
|
+
createsTerms: ruleCreatesTerms(rule, options),
|
|
4678
4775
|
};
|
|
4679
4776
|
});
|
|
4680
4777
|
|
|
4681
4778
|
const edgeMap = new Map();
|
|
4682
|
-
function addEdge(from, to,
|
|
4779
|
+
function addEdge(from, to, kind, predicate) {
|
|
4683
4780
|
const label = predicate || '*';
|
|
4684
4781
|
const key = `${from.index}->${to.index}:${label}`;
|
|
4782
|
+
const negated = kind === 'negated';
|
|
4783
|
+
const termGeneration = kind === 'term-generation';
|
|
4685
4784
|
const existing = edgeMap.get(key);
|
|
4686
4785
|
if (existing) {
|
|
4687
|
-
existing.
|
|
4786
|
+
existing.negated = existing.negated || negated;
|
|
4787
|
+
existing.termGeneration = existing.termGeneration || termGeneration;
|
|
4788
|
+
existing.negative = existing.negated || existing.termGeneration;
|
|
4688
4789
|
return;
|
|
4689
4790
|
}
|
|
4690
|
-
edgeMap.set(key, {
|
|
4791
|
+
edgeMap.set(key, {
|
|
4792
|
+
from: from.index,
|
|
4793
|
+
to: to.index,
|
|
4794
|
+
negative: negated || termGeneration,
|
|
4795
|
+
negated,
|
|
4796
|
+
termGeneration,
|
|
4797
|
+
predicate,
|
|
4798
|
+
});
|
|
4691
4799
|
}
|
|
4692
4800
|
|
|
4693
4801
|
const headIndex = buildHeadTemplateIndex(rules);
|
|
4694
4802
|
|
|
4695
4803
|
for (const from of rules) {
|
|
4696
|
-
const forceClosed = from.
|
|
4804
|
+
const forceClosed = from.createsTerms;
|
|
4697
4805
|
for (const pattern of from.positivePatterns) {
|
|
4698
4806
|
for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
|
|
4699
|
-
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed, dependencyPredicateLabel(pattern));
|
|
4807
|
+
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed ? 'term-generation' : 'positive', dependencyPredicateLabel(pattern));
|
|
4700
4808
|
}
|
|
4701
4809
|
}
|
|
4702
4810
|
for (const pattern of from.negativePatterns) {
|
|
4703
4811
|
for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
|
|
4704
|
-
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex],
|
|
4812
|
+
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], 'negated', dependencyPredicateLabel(pattern));
|
|
4705
4813
|
}
|
|
4706
4814
|
}
|
|
4707
4815
|
}
|
|
@@ -4717,7 +4825,7 @@
|
|
|
4717
4825
|
const unstratifiedCycles = [];
|
|
4718
4826
|
const seen = new Set();
|
|
4719
4827
|
for (const edge of edges) {
|
|
4720
|
-
if (!edge.
|
|
4828
|
+
if (!edge.negated) continue;
|
|
4721
4829
|
if (edge.from === edge.to && rules[edge.from].runOnce && !rules[edge.from].headHasBlankNode) continue;
|
|
4722
4830
|
if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
|
|
4723
4831
|
const component = components[componentOf.get(edge.from)];
|
|
@@ -4740,7 +4848,10 @@
|
|
|
4740
4848
|
positivePredicates: Array.from(rule.positivePredicates),
|
|
4741
4849
|
negativePredicates: Array.from(rule.negativePredicates),
|
|
4742
4850
|
runOnce: rule.runOnce,
|
|
4851
|
+
hasAssignment: rule.hasAssignment,
|
|
4852
|
+
hasTermGeneratingAssignment: rule.hasTermGeneratingAssignment,
|
|
4743
4853
|
headHasBlankNode: rule.headHasBlankNode,
|
|
4854
|
+
createsTerms: rule.createsTerms,
|
|
4744
4855
|
})),
|
|
4745
4856
|
edges,
|
|
4746
4857
|
components: components.map((component) => component.map((ruleIndex) => rules[ruleIndex].name)),
|
|
@@ -4910,7 +5021,14 @@
|
|
|
4910
5021
|
function recursiveRuleIndexes(dependency) {
|
|
4911
5022
|
const out = new Set();
|
|
4912
5023
|
const ruleByName = new Map(dependency.rules.map((rule) => [rule.name, rule]));
|
|
4913
|
-
const
|
|
5024
|
+
const componentOf = new Map();
|
|
5025
|
+
|
|
5026
|
+
dependency.components.forEach((component, componentIndex) => {
|
|
5027
|
+
for (const name of component) {
|
|
5028
|
+
const rule = ruleByName.get(name);
|
|
5029
|
+
if (rule) componentOf.set(rule.index, componentIndex);
|
|
5030
|
+
}
|
|
5031
|
+
});
|
|
4914
5032
|
|
|
4915
5033
|
for (const component of dependency.components) {
|
|
4916
5034
|
if (component.length <= 1) continue;
|
|
@@ -4921,9 +5039,9 @@
|
|
|
4921
5039
|
}
|
|
4922
5040
|
|
|
4923
5041
|
for (const edge of dependency.edges) {
|
|
4924
|
-
const rule =
|
|
4925
|
-
if (edge.from === edge.to && edge.
|
|
4926
|
-
out.add(edge.from);
|
|
5042
|
+
const rule = (dependency.rules || []).find((candidate) => candidate.index === edge.from);
|
|
5043
|
+
if (edge.from === edge.to && edge.negated && rule && rule.runOnce && !rule.headHasBlankNode) continue;
|
|
5044
|
+
if (edge.from === edge.to || componentOf.get(edge.from) === componentOf.get(edge.to)) out.add(edge.from);
|
|
4927
5045
|
}
|
|
4928
5046
|
return out;
|
|
4929
5047
|
}
|
|
@@ -5196,7 +5314,26 @@
|
|
|
5196
5314
|
}
|
|
5197
5315
|
|
|
5198
5316
|
function ruleHasAssignment(rule, options = {}) {
|
|
5199
|
-
return
|
|
5317
|
+
return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
|
|
5318
|
+
}
|
|
5319
|
+
|
|
5320
|
+
function ruleHasTermGeneratingAssignment(rule, options = {}) {
|
|
5321
|
+
return (rule.body || []).some((clause) => (clause.type === 'set' || clause.type === 'bind') && assignmentMayCreateNewTerm(clause.expr));
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
function ruleCreatesTerms(rule, options = {}) {
|
|
5325
|
+
return ruleHeadHasBlankNode(rule) || ruleHasTermGeneratingAssignment(rule, options);
|
|
5326
|
+
}
|
|
5327
|
+
|
|
5328
|
+
function assignmentMayCreateNewTerm(expr) {
|
|
5329
|
+
// Simple aliases and constants are not term generators: they select from a
|
|
5330
|
+
// fixed term or from already-bound terms. Expressions that compute values
|
|
5331
|
+
// through operators or functions may create an unbounded sequence when used
|
|
5332
|
+
// recursively, e.g. SET(?v1 := ?v + 1), so they remain part of the strict
|
|
5333
|
+
// recursive-new-terms check.
|
|
5334
|
+
if (!expr) return false;
|
|
5335
|
+
if (expr.type === 'var' || expr.type === 'term' || expr.type === 'literal') return false;
|
|
5336
|
+
return true;
|
|
5200
5337
|
}
|
|
5201
5338
|
|
|
5202
5339
|
function ruleHeadHasBlankNode(rule) {
|
package/examples/assignment.srl
CHANGED
|
@@ -15,6 +15,6 @@ RULE {
|
|
|
15
15
|
} WHERE {
|
|
16
16
|
?person :name ?name ;
|
|
17
17
|
:age ?age .
|
|
18
|
-
FILTER(datatype(?age) = xsd:integer && ?age >= 18 && lang(?name) = "en")
|
|
18
|
+
FILTER(datatype(?age) = xsd:integer && ?age >= 18 && lang(?name) = "en")
|
|
19
19
|
SET(?slug := REPLACE(LCASE(STR(?name)), " ", "-"))
|
|
20
20
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
_:
|
|
2
|
-
_:
|
|
1
|
+
_:sk_319b8a2f709c2e2189803dc77b3d3e85 a :BritishShortHair .
|
|
2
|
+
_:sk_bb32b39908778273d11a8e49e72b9547 a :Cat .
|
|
3
3
|
:test :is true .
|
|
@@ -96485,7 +96485,7 @@ WHERE {
|
|
|
96485
96485
|
?query a :RouteQuery ;
|
|
96486
96486
|
:source ?source .
|
|
96487
96487
|
?source nepo:hasOutboundRouteTo ?next .
|
|
96488
|
-
FILTER(!sameTerm(?source, ?next))
|
|
96488
|
+
FILTER(!sameTerm(?source, ?next))
|
|
96489
96489
|
?source rdfs:label ?sourceName .
|
|
96490
96490
|
?next rdfs:label ?nextName .
|
|
96491
96491
|
SET(?pathKey := CONCAT("|", STR(?source), "|", STR(?next), "|"))
|
|
@@ -96514,12 +96514,12 @@ WHERE {
|
|
|
96514
96514
|
:routeLabel ?routeLabel .
|
|
96515
96515
|
?query :destination ?destination ;
|
|
96516
96516
|
:maxStopOvers ?maxStopOvers .
|
|
96517
|
-
FILTER(!sameTerm(?airport, ?destination))
|
|
96518
|
-
FILTER(?stopovers < ?maxStopOvers)
|
|
96517
|
+
FILTER(!sameTerm(?airport, ?destination))
|
|
96518
|
+
FILTER(?stopovers < ?maxStopOvers)
|
|
96519
96519
|
?airport nepo:hasOutboundRouteTo ?next .
|
|
96520
|
-
FILTER(!sameTerm(?airport, ?next))
|
|
96520
|
+
FILTER(!sameTerm(?airport, ?next))
|
|
96521
96521
|
SET(?nextNeedle := CONCAT("|", STR(?next), "|"))
|
|
96522
|
-
FILTER(!CONTAINS(?pathKey, ?nextNeedle))
|
|
96522
|
+
FILTER(!CONTAINS(?pathKey, ?nextNeedle))
|
|
96523
96523
|
?next rdfs:label ?nextName .
|
|
96524
96524
|
SET(?nextStopovers := ?stopovers + 1)
|
|
96525
96525
|
SET(?nextPathKey := CONCAT(?pathKey, STR(?next), "|"))
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
4823
|
-
const recursiveIndexes =
|
|
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
|
-
|
|
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 || {})}
|
|
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,
|
|
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.
|
|
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, {
|
|
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.
|
|
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],
|
|
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.
|
|
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
|
|
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 =
|
|
5155
|
-
if (edge.from === edge.to && edge.
|
|
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
|
|
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
package/src/analyze.js
CHANGED
|
@@ -6,8 +6,8 @@ const { tripleHasBlankNode } = require('./assignments.js');
|
|
|
6
6
|
function analyze(program, options = {}) {
|
|
7
7
|
const diagnostics = [];
|
|
8
8
|
const dependency = dependencyGraph(program, options);
|
|
9
|
-
const
|
|
10
|
-
const recursiveIndexes =
|
|
9
|
+
const hasTermGeneratingRules = dependency.rules.some((rule) => rule.createsTerms);
|
|
10
|
+
const recursiveIndexes = hasTermGeneratingRules ? recursiveRuleIndexes(dependency) : new Set();
|
|
11
11
|
|
|
12
12
|
program.rules.forEach((rule, index) => {
|
|
13
13
|
const name = ruleName(rule, index);
|
|
@@ -39,12 +39,13 @@ function analyze(program, options = {}) {
|
|
|
39
39
|
|
|
40
40
|
diagnostics.push(...sequentialWellFormednessDiagnostics(rule.body, name, program.prefixes || {}));
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
const depRule = dependency.rules[index] || {};
|
|
43
|
+
if (depRule.createsTerms && recursiveIndexes.has(index)) {
|
|
43
44
|
diagnostics.push({
|
|
44
45
|
code: 'recursive-assignment-rule',
|
|
45
46
|
severity: 'warning',
|
|
46
47
|
rule: name,
|
|
47
|
-
message: `${displayRuleName(name, program.prefixes || {})}
|
|
48
|
+
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)`,
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -91,34 +92,47 @@ function dependencyGraph(program, options = {}) {
|
|
|
91
92
|
negativePredicates: new Set(negativePatterns.flatMap((triple) => predicateIRIs(triple))),
|
|
92
93
|
runOnce: !!rule.runOnce,
|
|
93
94
|
hasAssignment: ruleHasAssignment(rule, options),
|
|
95
|
+
hasTermGeneratingAssignment: ruleHasTermGeneratingAssignment(rule, options),
|
|
94
96
|
headHasBlankNode: ruleHeadHasBlankNode(rule),
|
|
97
|
+
createsTerms: ruleCreatesTerms(rule, options),
|
|
95
98
|
};
|
|
96
99
|
});
|
|
97
100
|
|
|
98
101
|
const edgeMap = new Map();
|
|
99
|
-
function addEdge(from, to,
|
|
102
|
+
function addEdge(from, to, kind, predicate) {
|
|
100
103
|
const label = predicate || '*';
|
|
101
104
|
const key = `${from.index}->${to.index}:${label}`;
|
|
105
|
+
const negated = kind === 'negated';
|
|
106
|
+
const termGeneration = kind === 'term-generation';
|
|
102
107
|
const existing = edgeMap.get(key);
|
|
103
108
|
if (existing) {
|
|
104
|
-
existing.
|
|
109
|
+
existing.negated = existing.negated || negated;
|
|
110
|
+
existing.termGeneration = existing.termGeneration || termGeneration;
|
|
111
|
+
existing.negative = existing.negated || existing.termGeneration;
|
|
105
112
|
return;
|
|
106
113
|
}
|
|
107
|
-
edgeMap.set(key, {
|
|
114
|
+
edgeMap.set(key, {
|
|
115
|
+
from: from.index,
|
|
116
|
+
to: to.index,
|
|
117
|
+
negative: negated || termGeneration,
|
|
118
|
+
negated,
|
|
119
|
+
termGeneration,
|
|
120
|
+
predicate,
|
|
121
|
+
});
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
const headIndex = buildHeadTemplateIndex(rules);
|
|
111
125
|
|
|
112
126
|
for (const from of rules) {
|
|
113
|
-
const forceClosed = from.
|
|
127
|
+
const forceClosed = from.createsTerms;
|
|
114
128
|
for (const pattern of from.positivePatterns) {
|
|
115
129
|
for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
|
|
116
|
-
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed, dependencyPredicateLabel(pattern));
|
|
130
|
+
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed ? 'term-generation' : 'positive', dependencyPredicateLabel(pattern));
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
for (const pattern of from.negativePatterns) {
|
|
120
134
|
for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
|
|
121
|
-
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex],
|
|
135
|
+
if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], 'negated', dependencyPredicateLabel(pattern));
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
}
|
|
@@ -134,7 +148,7 @@ function dependencyGraph(program, options = {}) {
|
|
|
134
148
|
const unstratifiedCycles = [];
|
|
135
149
|
const seen = new Set();
|
|
136
150
|
for (const edge of edges) {
|
|
137
|
-
if (!edge.
|
|
151
|
+
if (!edge.negated) continue;
|
|
138
152
|
if (edge.from === edge.to && rules[edge.from].runOnce && !rules[edge.from].headHasBlankNode) continue;
|
|
139
153
|
if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
|
|
140
154
|
const component = components[componentOf.get(edge.from)];
|
|
@@ -157,7 +171,10 @@ function dependencyGraph(program, options = {}) {
|
|
|
157
171
|
positivePredicates: Array.from(rule.positivePredicates),
|
|
158
172
|
negativePredicates: Array.from(rule.negativePredicates),
|
|
159
173
|
runOnce: rule.runOnce,
|
|
174
|
+
hasAssignment: rule.hasAssignment,
|
|
175
|
+
hasTermGeneratingAssignment: rule.hasTermGeneratingAssignment,
|
|
160
176
|
headHasBlankNode: rule.headHasBlankNode,
|
|
177
|
+
createsTerms: rule.createsTerms,
|
|
161
178
|
})),
|
|
162
179
|
edges,
|
|
163
180
|
components: components.map((component) => component.map((ruleIndex) => rules[ruleIndex].name)),
|
|
@@ -327,7 +344,14 @@ function componentMin(component) {
|
|
|
327
344
|
function recursiveRuleIndexes(dependency) {
|
|
328
345
|
const out = new Set();
|
|
329
346
|
const ruleByName = new Map(dependency.rules.map((rule) => [rule.name, rule]));
|
|
330
|
-
const
|
|
347
|
+
const componentOf = new Map();
|
|
348
|
+
|
|
349
|
+
dependency.components.forEach((component, componentIndex) => {
|
|
350
|
+
for (const name of component) {
|
|
351
|
+
const rule = ruleByName.get(name);
|
|
352
|
+
if (rule) componentOf.set(rule.index, componentIndex);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
331
355
|
|
|
332
356
|
for (const component of dependency.components) {
|
|
333
357
|
if (component.length <= 1) continue;
|
|
@@ -338,9 +362,9 @@ function recursiveRuleIndexes(dependency) {
|
|
|
338
362
|
}
|
|
339
363
|
|
|
340
364
|
for (const edge of dependency.edges) {
|
|
341
|
-
const rule =
|
|
342
|
-
if (edge.from === edge.to && edge.
|
|
343
|
-
out.add(edge.from);
|
|
365
|
+
const rule = (dependency.rules || []).find((candidate) => candidate.index === edge.from);
|
|
366
|
+
if (edge.from === edge.to && edge.negated && rule && rule.runOnce && !rule.headHasBlankNode) continue;
|
|
367
|
+
if (edge.from === edge.to || componentOf.get(edge.from) === componentOf.get(edge.to)) out.add(edge.from);
|
|
344
368
|
}
|
|
345
369
|
return out;
|
|
346
370
|
}
|
|
@@ -613,7 +637,26 @@ function substituteAssignedConstant(term, constants) {
|
|
|
613
637
|
}
|
|
614
638
|
|
|
615
639
|
function ruleHasAssignment(rule, options = {}) {
|
|
616
|
-
return
|
|
640
|
+
return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function ruleHasTermGeneratingAssignment(rule, options = {}) {
|
|
644
|
+
return (rule.body || []).some((clause) => (clause.type === 'set' || clause.type === 'bind') && assignmentMayCreateNewTerm(clause.expr));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function ruleCreatesTerms(rule, options = {}) {
|
|
648
|
+
return ruleHeadHasBlankNode(rule) || ruleHasTermGeneratingAssignment(rule, options);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function assignmentMayCreateNewTerm(expr) {
|
|
652
|
+
// Simple aliases and constants are not term generators: they select from a
|
|
653
|
+
// fixed term or from already-bound terms. Expressions that compute values
|
|
654
|
+
// through operators or functions may create an unbounded sequence when used
|
|
655
|
+
// recursively, e.g. SET(?v1 := ?v + 1), so they remain part of the strict
|
|
656
|
+
// recursive-new-terms check.
|
|
657
|
+
if (!expr) return false;
|
|
658
|
+
if (expr.type === 'var' || expr.type === 'term' || expr.type === 'literal') return false;
|
|
659
|
+
return true;
|
|
617
660
|
}
|
|
618
661
|
|
|
619
662
|
function ruleHeadHasBlankNode(rule) {
|
package/src/cli.js
CHANGED
|
@@ -34,7 +34,7 @@ function readPackageVersion() {
|
|
|
34
34
|
const VERSION = readPackageVersion();
|
|
35
35
|
|
|
36
36
|
function help() {
|
|
37
|
-
return `eyeleng ${VERSION}\n\nA dependency-free JavaScript implementation experiment for the SHACL 1.2 Rules draft, including SRL and RDF Rules syntax front-ends.\n\nUsage:\n eyeleng [options] [file ...]\n\nOptions:\n --all Print the full closure, including input facts\n --json Print JSON instead of compact triples/bindings\n --trace Print derivation trace to stderr, or include it in JSON\n --stats Print iteration and triple counts to stderr\n --check Parse and analyze only; do not run rules\n --strict Treat static warnings as errors\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`;
|
|
37
|
+
return `eyeleng ${VERSION}\n\nA dependency-free JavaScript implementation experiment for the SHACL 1.2 Rules draft, including SRL and RDF Rules syntax front-ends.\n\nUsage:\n eyeleng [options] [file ...]\n\nOptions:\n --all Print the full closure, including input facts\n --json Print JSON instead of compact triples/bindings\n --trace Print derivation trace to stderr, or include it in JSON\n --stats Print iteration and triple counts to stderr\n --check Parse and analyze only; do not run rules\n --strict Treat static warnings as errors, including recursive term generation\n --deps Print rule dependency edges during --check\n --query TEXT Run a raw SRL body pattern over the closure\n --query-file FILE Read a raw SRL body pattern from a file\n --max-iterations N Stop after N fixpoint iterations within a recursive layer\n --no-imports Parse IMPORTS/owl:imports but do not load imported rule sets\n --rdf-messages Parse input as an RDF Message Log\n --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`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function parseArgs(argv) {
|
|
@@ -142,7 +142,7 @@ function printDependencies(analysis, prefixes, stderr) {
|
|
|
142
142
|
for (const edge of edges) {
|
|
143
143
|
const from = formatRuleName(analysis.dependency.rules[edge.from].name, prefixes);
|
|
144
144
|
const to = formatRuleName(analysis.dependency.rules[edge.to].name, prefixes);
|
|
145
|
-
const kind = edge.
|
|
145
|
+
const kind = edge.negated ? 'NOT' : (edge.termGeneration ? 'generates' : 'uses');
|
|
146
146
|
stderr.write(`eyeleng: deps: ${from} --${kind} ${edge.predicate ? compactIRI(edge.predicate, prefixes) : '*'}--> ${to}\n`);
|
|
147
147
|
}
|
|
148
148
|
if (analysis.dependency.layers && analysis.dependency.layers.length > 0) {
|
package/src/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { TripleStore, bindingKey
|
|
4
|
-
const { tripleKey, termEquals } = require('./term.js');
|
|
3
|
+
const { TripleStore, bindingKey } = require('./store.js');
|
|
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
7
|
|
|
@@ -21,7 +21,7 @@ function evaluate(program, options = {}) {
|
|
|
21
21
|
runOnce: !!rule.runOnce,
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
|
-
const analysis = options.analysis || analyze(program);
|
|
24
|
+
const analysis = options.analysis || analyze(program, options);
|
|
25
25
|
if (analysis.errors && analysis.errors.length > 0 && !options.ignoreAnalysisErrors) {
|
|
26
26
|
throw new Error(`Analysis failed: ${analysis.errors.map((error) => error.message).join('; ')}`);
|
|
27
27
|
}
|
|
@@ -32,6 +32,9 @@ function evaluate(program, options = {}) {
|
|
|
32
32
|
layerIndexes,
|
|
33
33
|
analysis.dependency ? analysis.dependency.edges : [],
|
|
34
34
|
);
|
|
35
|
+
const relaxedRecursiveRunOnce = options.relaxedRecursion === false
|
|
36
|
+
? new Set()
|
|
37
|
+
: recursiveTermGenerationRuleIndexes(analysis);
|
|
35
38
|
const baseContext = {
|
|
36
39
|
...evalOptions,
|
|
37
40
|
maxIterations,
|
|
@@ -47,8 +50,8 @@ function evaluate(program, options = {}) {
|
|
|
47
50
|
|
|
48
51
|
for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
|
|
49
52
|
const layer = layerIndexes[layerIndex];
|
|
50
|
-
const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce);
|
|
51
|
-
const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce);
|
|
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));
|
|
52
55
|
|
|
53
56
|
if (runOnce.length > 0) {
|
|
54
57
|
iterations += 1;
|
|
@@ -148,6 +151,7 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
148
151
|
let added = 0;
|
|
149
152
|
const dedupeBindings = rule.body.some((clause) => clause.type === 'path');
|
|
150
153
|
const seenBindings = dedupeBindings ? new Set() : null;
|
|
154
|
+
const headBlankLabels = collectHeadBlankLabels(rule.head);
|
|
151
155
|
|
|
152
156
|
const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
|
|
153
157
|
? store.match(rule.body[0].triple, {})
|
|
@@ -162,8 +166,10 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
162
166
|
applications += 1;
|
|
163
167
|
context.perRule[ruleIndex].applications += 1;
|
|
164
168
|
|
|
169
|
+
const headBlankMap = headBlankLabels.size > 0 ? new Map() : null;
|
|
170
|
+
const skolemKey = headBlankMap ? skolemizationKey(ruleIndex, binding) : null;
|
|
165
171
|
for (const head of rule.head) {
|
|
166
|
-
const triple =
|
|
172
|
+
const triple = instantiateHeadTriple(head, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
167
173
|
if (!triple) continue;
|
|
168
174
|
if (store.add(triple)) {
|
|
169
175
|
added += 1;
|
|
@@ -185,6 +191,94 @@ function applyRuleOnce(program, store, ruleIndex, context) {
|
|
|
185
191
|
return { applications, added };
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
function recursiveTermGenerationRuleIndexes(analysis) {
|
|
195
|
+
const out = new Set();
|
|
196
|
+
if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
|
|
197
|
+
const byName = new Map((analysis.dependency.rules || []).map((rule) => [rule.name, rule.index]));
|
|
198
|
+
for (const diagnostic of analysis.diagnostics) {
|
|
199
|
+
if (diagnostic.code !== 'recursive-assignment-rule') continue;
|
|
200
|
+
if (byName.has(diagnostic.rule)) out.add(byName.get(diagnostic.rule));
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function instantiateHeadTriple(pattern, binding, headBlankLabels, headBlankMap, skolemKey) {
|
|
206
|
+
const s = instantiateHeadTerm(pattern.s, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
207
|
+
const p = instantiateHeadTerm(pattern.p, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
208
|
+
const o = instantiateHeadTerm(pattern.o, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
209
|
+
if (!s || !p || !o) return null;
|
|
210
|
+
if (p.type !== 'iri') return null;
|
|
211
|
+
return { s, p, o };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function instantiateHeadTerm(term, binding, headBlankLabels, headBlankMap, skolemKey) {
|
|
215
|
+
if (term.type === 'var') return binding[term.value] || null;
|
|
216
|
+
if (term.type === 'blank' && headBlankLabels.has(term.value)) {
|
|
217
|
+
let label = headBlankMap.get(term.value);
|
|
218
|
+
if (!label) {
|
|
219
|
+
label = `sk_${deterministicSkolemIdFromKey(`${skolemKey}|${term.value}`).replace(/-/g, '_')}`;
|
|
220
|
+
headBlankMap.set(term.value, label);
|
|
221
|
+
}
|
|
222
|
+
return blankNode(label);
|
|
223
|
+
}
|
|
224
|
+
if (term.type === 'triple') {
|
|
225
|
+
const s = instantiateHeadTerm(term.s, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
226
|
+
const p = instantiateHeadTerm(term.p, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
227
|
+
const o = instantiateHeadTerm(term.o, binding, headBlankLabels, headBlankMap, skolemKey);
|
|
228
|
+
if (!s || !p || !o) return null;
|
|
229
|
+
return tripleTerm(s, p, o);
|
|
230
|
+
}
|
|
231
|
+
return term;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function collectHeadBlankLabels(head) {
|
|
235
|
+
const labels = new Set();
|
|
236
|
+
for (const triple of head || []) {
|
|
237
|
+
collectBlankLabelsFromTerm(triple.s, labels);
|
|
238
|
+
collectBlankLabelsFromTerm(triple.p, labels);
|
|
239
|
+
collectBlankLabelsFromTerm(triple.o, labels);
|
|
240
|
+
}
|
|
241
|
+
return labels;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function collectBlankLabelsFromTerm(term, labels) {
|
|
245
|
+
if (!term) return;
|
|
246
|
+
if (term.type === 'blank') {
|
|
247
|
+
labels.add(term.value);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (term.type === 'triple') {
|
|
251
|
+
collectBlankLabelsFromTerm(term.s, labels);
|
|
252
|
+
collectBlankLabelsFromTerm(term.p, labels);
|
|
253
|
+
collectBlankLabelsFromTerm(term.o, labels);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function skolemizationKey(ruleIndex, binding) {
|
|
258
|
+
let out = `rule:${ruleIndex}`;
|
|
259
|
+
for (const name of Object.keys(binding).sort()) {
|
|
260
|
+
const value = binding[name];
|
|
261
|
+
out += `|${name}=${value ? termKey(value) : 'unbound'}`;
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function deterministicSkolemIdFromKey(key) {
|
|
267
|
+
let h1 = 0x811c9dc5;
|
|
268
|
+
let h2 = 0x811c9dc5;
|
|
269
|
+
let h3 = 0x811c9dc5;
|
|
270
|
+
let h4 = 0x811c9dc5;
|
|
271
|
+
for (let i = 0; i < key.length; i += 1) {
|
|
272
|
+
const c = key.charCodeAt(i);
|
|
273
|
+
h1 ^= c; h1 = Math.imul(h1, 0x01000193) >>> 0;
|
|
274
|
+
h2 ^= c + 1; h2 = Math.imul(h2, 0x01000193) >>> 0;
|
|
275
|
+
h3 ^= c + 2; h3 = Math.imul(h3, 0x01000193) >>> 0;
|
|
276
|
+
h4 ^= c + 3; h4 = Math.imul(h4, 0x01000193) >>> 0;
|
|
277
|
+
}
|
|
278
|
+
return [h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, '0')).join('');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
188
282
|
function evaluateBody(clauses, store, initialBinding = {}, options = {}) {
|
|
189
283
|
const bindings = [];
|
|
190
284
|
const seen = new Set();
|
|
@@ -139,6 +139,10 @@ async function runSyntaxOrWellformedTest(test, options = {}) {
|
|
|
139
139
|
filename: test.actionUrl,
|
|
140
140
|
baseIRI: test.actionUrl,
|
|
141
141
|
shacl12Conformance: true,
|
|
142
|
+
// The W3C manifest defines the strict, guaranteed-terminating conformance profile.
|
|
143
|
+
// Eyeleng's relaxed mode remains available through the normal API/CLI, but
|
|
144
|
+
// negative well-formedness tests for recursive term generation must fail here.
|
|
145
|
+
strict: true,
|
|
142
146
|
};
|
|
143
147
|
|
|
144
148
|
if (test.type.includes('Syntax')) {
|
|
@@ -176,6 +180,7 @@ async function runEvalTest(test, options = {}) {
|
|
|
176
180
|
filename: test.rulesetUrl,
|
|
177
181
|
baseIRI: test.rulesetUrl,
|
|
178
182
|
shacl12Conformance: true,
|
|
183
|
+
strict: true,
|
|
179
184
|
};
|
|
180
185
|
const compiled = eyeleng.compile(rulesSource, compileOptions);
|
|
181
186
|
const program = { ...compiled.program, data: [...compiled.program.data, ...dataTriples] };
|
package/test/api.test.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { test, main } = require('./harness.js').createHarness('API');
|
|
4
4
|
const assert = require('node:assert/strict');
|
|
5
5
|
const { parse, compile, run, runToString } = require('../src/index.js');
|
|
6
|
+
const { tripleKey } = require('../src/term.js');
|
|
6
7
|
|
|
7
8
|
test('parse reads prefixes, data, and rules', () => {
|
|
8
9
|
const program = parse(`
|
|
@@ -455,7 +456,70 @@ RULE { :s :p ?x } WHERE { :s :q ?x SET(?z := STR(?x)) }
|
|
|
455
456
|
`, options));
|
|
456
457
|
});
|
|
457
458
|
|
|
458
|
-
|
|
459
|
+
|
|
460
|
+
test('relaxed mode permits recursive deterministic assignments with max-iteration safety', () => {
|
|
461
|
+
const source = `
|
|
462
|
+
PREFIX : <http://example/>
|
|
463
|
+
DATA { :counter :value 0 . :limit :max 3 . }
|
|
464
|
+
RULE { :counter :value ?next } WHERE {
|
|
465
|
+
:counter :value ?value .
|
|
466
|
+
:limit :max ?max .
|
|
467
|
+
FILTER(?value < ?max)
|
|
468
|
+
SET(?next := ?value + 1)
|
|
469
|
+
}
|
|
470
|
+
`;
|
|
471
|
+
const compiled = compile(source, { shacl12Conformance: true, throwOnDiagnostics: false });
|
|
472
|
+
assert.equal(compiled.analysis.warnings[0].code, 'recursive-assignment-rule');
|
|
473
|
+
assert.throws(() => compile(source, { shacl12Conformance: true, strict: true }), /termination is not guaranteed/);
|
|
474
|
+
const output = runToString(source, { shacl12Conformance: true, maxIterations: 20 });
|
|
475
|
+
assert.match(output, /:counter :value 1 \./);
|
|
476
|
+
assert.match(output, /:counter :value 2 \./);
|
|
477
|
+
assert.match(output, /:counter :value 3 \./);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('head blank nodes are deterministically skolemized with all universal bindings', () => {
|
|
481
|
+
const source = `
|
|
482
|
+
PREFIX : <http://example/>
|
|
483
|
+
DATA { :a :p :o . :b :p :o . }
|
|
484
|
+
RULE { [] :source ?s ; :value :o } WHERE { ?s :p :o }
|
|
485
|
+
`;
|
|
486
|
+
const first = run(source);
|
|
487
|
+
const second = run(source);
|
|
488
|
+
assert.deepEqual(first.inferred.map(tripleKey).sort(), second.inferred.map(tripleKey).sort());
|
|
489
|
+
const sourcePredicate = 'http://example/source';
|
|
490
|
+
const witnessSubjects = first.inferred
|
|
491
|
+
.filter((triple) => triple.p.type === 'iri' && triple.p.value === sourcePredicate)
|
|
492
|
+
.map((triple) => triple.s.value);
|
|
493
|
+
assert.equal(witnessSubjects.length, 2);
|
|
494
|
+
assert.equal(new Set(witnessSubjects).size, 2);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
test('head blank skolemization is a standard function of all universals', () => {
|
|
499
|
+
const source = `
|
|
500
|
+
PREFIX : <http://example.org/#>
|
|
501
|
+
DATA { :s1 :p :o . :s2 :p :o . }
|
|
502
|
+
RULE { [] :witnessFor ?s } WHERE { ?s :p :o }
|
|
503
|
+
`;
|
|
504
|
+
const result = run(source, { maxIterations: 20, throwOnDiagnostics: false });
|
|
505
|
+
const witnessSubjects = result.inferred
|
|
506
|
+
.filter((triple) => triple.p.value === 'http://example.org/#witnessFor')
|
|
507
|
+
.map((triple) => triple.s.value);
|
|
508
|
+
assert.equal(witnessSubjects.length, 2);
|
|
509
|
+
assert.equal(new Set(witnessSubjects).size, 2);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('recursive existential rules may not terminate with all-universal skolemization', () => {
|
|
513
|
+
const source = `
|
|
514
|
+
PREFIX : <http://example.org/#>
|
|
515
|
+
DATA { :s :p :o . }
|
|
516
|
+
RULE { [] :p :o } WHERE { ?s :p :o }
|
|
517
|
+
`;
|
|
518
|
+
assert.throws(
|
|
519
|
+
() => run(source, { maxIterations: 5, throwOnDiagnostics: false }),
|
|
520
|
+
/Reached maxIterations=5/,
|
|
521
|
+
);
|
|
522
|
+
});
|
|
459
523
|
|
|
460
524
|
test('RDF Message Logs expose Eyeling-style envelopes and payload triples', () => {
|
|
461
525
|
const messages = `VERSION "1.2-messages"
|
|
@@ -519,3 +583,20 @@ RULE { :test :annotation ?source } WHERE {
|
|
|
519
583
|
assert.match(output, /:root :first 1 \./);
|
|
520
584
|
assert.match(output, /:test :annotation :witness \./);
|
|
521
585
|
});
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
test('strict conformance allows recursive constant BIND aliases', () => {
|
|
589
|
+
assert.doesNotThrow(() => compile(`
|
|
590
|
+
PREFIX : <http://example.org/#>
|
|
591
|
+
RULE { ?s ?p ?o } WHERE { ?s :p ?o . BIND(:p AS ?p) }
|
|
592
|
+
`, { shacl12Conformance: true, strict: true }));
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('strict conformance rejects recursive computed assignments', () => {
|
|
596
|
+
assert.throws(() => compile(`
|
|
597
|
+
PREFIX : <http://example.org/#>
|
|
598
|
+
RULE { ?x :p ?v1 } WHERE { ?x :p ?v . SET(?v1 := ?v + 1) }
|
|
599
|
+
`, { shacl12Conformance: true, strict: true }), /creates terms in a recursive dependency cycle/);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
main();
|