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/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 +5 -4
- package/reports/w3c-shacl12-rules-earl.ttl +1055 -1336
- 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/test/run.js +0 -1
- package/tools/w3c-rdf.js +11 -7
- package/tools/w3c-shacl12-rules.js +11 -6
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();
|
package/test/run.js
CHANGED
package/tools/w3c-rdf.js
CHANGED
|
@@ -8,7 +8,6 @@ const {
|
|
|
8
8
|
runW3cRdfManifests,
|
|
9
9
|
formatW3cRdfProgressLine,
|
|
10
10
|
formatW3cRdfManifestsResult,
|
|
11
|
-
rdfManifestsToEarl,
|
|
12
11
|
writeRdfEarlReport,
|
|
13
12
|
defaultRdfReportPath,
|
|
14
13
|
} = require('../src/rdfManifest.js');
|
|
@@ -19,12 +18,18 @@ function argValue(argv, name) {
|
|
|
19
18
|
return argv[index + 1] || null;
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
function loggerForMode({ json, earl }) {
|
|
22
|
+
// Keep stdout machine-readable or empty in output modes. Progress still stays visible.
|
|
23
|
+
return json || earl ? console.error : console.log;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
async function main(argv = process.argv.slice(2)) {
|
|
23
27
|
const json = argv.includes('--json');
|
|
24
28
|
const earl = argv.includes('--earl');
|
|
25
29
|
const noReport = argv.includes('--no-report');
|
|
26
|
-
const quiet =
|
|
30
|
+
const quiet = argv.includes('--quiet');
|
|
27
31
|
const output = argValue(argv, '--output') || defaultRdfReportPath();
|
|
32
|
+
const log = loggerForMode({ json, earl });
|
|
28
33
|
const manifests = argv.filter((arg, index) => {
|
|
29
34
|
if (arg.startsWith('--')) return false;
|
|
30
35
|
if (argv[index - 1] === '--output') return false;
|
|
@@ -33,19 +38,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
33
38
|
const resources = manifests.length ? manifests : defaultW3cRdfManifestUrls;
|
|
34
39
|
const result = await runW3cRdfManifests(resources, {
|
|
35
40
|
onManifestStart(resource, index, total) {
|
|
36
|
-
if (!quiet)
|
|
41
|
+
if (!quiet) log(`${C.y}==${C.n} W3C RDF manifest ${index + 1}/${total}: ${resource}`);
|
|
37
42
|
},
|
|
38
43
|
onProgress(item, index) {
|
|
39
|
-
if (!quiet)
|
|
44
|
+
if (!quiet) log(formatW3cRdfProgressLine(item, index, { colors: C }));
|
|
40
45
|
},
|
|
41
46
|
});
|
|
42
47
|
if (!noReport) {
|
|
43
48
|
const reportPath = writeRdfEarlReport(result, output, { assertedBy: '<https://github.com/eyereasoner/eyeleng>' });
|
|
44
|
-
if (!quiet)
|
|
49
|
+
if (!quiet) log(`${C.dim}EARL report: ${path.relative(path.join(__dirname, '..'), reportPath)}${C.n}`);
|
|
45
50
|
}
|
|
46
51
|
if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
47
|
-
else if (earl) process.stdout.write(`${
|
|
48
|
-
else process.stdout.write(`${formatW3cRdfManifestsResult(result, { colors: C })}\n`);
|
|
52
|
+
else if (!earl) process.stdout.write(`${formatW3cRdfManifestsResult(result, { colors: C })}\n`);
|
|
49
53
|
return result.counts.fail === 0 ? 0 : 1;
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -8,7 +8,6 @@ const {
|
|
|
8
8
|
runShacl12RulesManifest,
|
|
9
9
|
formatShacl12RulesProgressLine,
|
|
10
10
|
formatShacl12RulesManifestResult,
|
|
11
|
-
shacl12RulesManifestToEarl,
|
|
12
11
|
writeShacl12RulesEarlReport,
|
|
13
12
|
defaultReportPath,
|
|
14
13
|
} = require('../src/shacl12RulesManifest.js');
|
|
@@ -19,12 +18,18 @@ function argValue(argv, name) {
|
|
|
19
18
|
return argv[index + 1] || null;
|
|
20
19
|
}
|
|
21
20
|
|
|
21
|
+
function loggerForMode({ json, earl }) {
|
|
22
|
+
// Keep stdout machine-readable or empty in output modes. Progress still stays visible.
|
|
23
|
+
return json || earl ? console.error : console.log;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
async function main(argv = process.argv.slice(2)) {
|
|
23
27
|
const json = argv.includes('--json');
|
|
24
28
|
const earl = argv.includes('--earl');
|
|
25
29
|
const noReport = argv.includes('--no-report');
|
|
26
|
-
const quiet =
|
|
30
|
+
const quiet = argv.includes('--quiet');
|
|
27
31
|
const output = argValue(argv, '--output') || defaultReportPath();
|
|
32
|
+
const log = loggerForMode({ json, earl });
|
|
28
33
|
const manifests = argv.filter((arg, index) => {
|
|
29
34
|
if (arg.startsWith('--')) return false;
|
|
30
35
|
if (argv[index - 1] === '--output') return false;
|
|
@@ -32,20 +37,20 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
32
37
|
});
|
|
33
38
|
const manifest = manifests[0] || process.env.EYELENG_SHACL12_RULES_MANIFEST || defaultShacl12RulesManifestUrl;
|
|
34
39
|
|
|
40
|
+
if (!quiet) log(`${C.y}==${C.n} W3C SHACL 1.2 Rules manifest: ${manifest}`);
|
|
35
41
|
const result = await runShacl12RulesManifest(manifest, {
|
|
36
42
|
onProgress(item, index) {
|
|
37
|
-
if (!quiet)
|
|
43
|
+
if (!quiet) log(formatShacl12RulesProgressLine(item, index, { colors: C }));
|
|
38
44
|
},
|
|
39
45
|
});
|
|
40
46
|
|
|
41
47
|
if (!noReport) {
|
|
42
48
|
const reportPath = writeShacl12RulesEarlReport(result, output);
|
|
43
|
-
if (!quiet)
|
|
49
|
+
if (!quiet) log(`${C.dim}EARL report: ${path.relative(path.join(__dirname, '..'), reportPath)}${C.n}`);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
47
|
-
else if (earl) process.stdout.write(`${
|
|
48
|
-
else process.stdout.write(`${formatShacl12RulesManifestResult(result, { colors: C })}\n`);
|
|
53
|
+
else if (!earl) process.stdout.write(`${formatShacl12RulesManifestResult(result, { colors: C })}\n`);
|
|
49
54
|
return result.counts.fail === 0 ? 0 : 1;
|
|
50
55
|
}
|
|
51
56
|
|