eyeling 1.29.2 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -0
- package/dist/browser/eyeling.browser.js +711 -21
- package/examples/input/rdf-surfaces-all-values-from-reverse.ttl +17 -0
- package/examples/input/rdf-surfaces-all-values-from.ttl +13 -0
- package/examples/input/rdf-surfaces-ancestor.ttl +20 -0
- package/examples/input/rdf-surfaces-city.ttl +11 -0
- package/examples/input/rdf-surfaces-domain.ttl +11 -0
- package/examples/input/rdf-surfaces-multi-premise.ttl +13 -0
- package/examples/input/rdf-surfaces-owl-all-values-from-codex.ttl +35 -0
- package/examples/input/rdf-surfaces-property-chain.ttl +13 -0
- package/examples/input/rdf-surfaces-range.ttl +11 -0
- package/examples/input/rdf-surfaces-rdfs-range-codex.ttl +18 -0
- package/examples/input/rdf-surfaces-rdfs-subclass-codex.ttl +18 -0
- package/examples/output/rdf-surfaces-all-values-from-reverse.n3 +3 -0
- package/examples/output/rdf-surfaces-all-values-from.n3 +3 -0
- package/examples/output/rdf-surfaces-ancestor.n3 +5 -0
- package/examples/output/rdf-surfaces-city.n3 +3 -0
- package/examples/output/rdf-surfaces-domain.n3 +3 -0
- package/examples/output/rdf-surfaces-multi-premise.n3 +3 -0
- package/examples/output/rdf-surfaces-owl-all-values-from-codex.n3 +6 -0
- package/examples/output/rdf-surfaces-property-chain.n3 +3 -0
- package/examples/output/rdf-surfaces-range.n3 +3 -0
- package/examples/output/rdf-surfaces-rdfs-range-codex.n3 +3 -0
- package/examples/output/rdf-surfaces-rdfs-subclass-codex.n3 +3 -0
- package/examples/rdf-surfaces-all-values-from-reverse.n3 +6 -0
- package/examples/rdf-surfaces-all-values-from.n3 +6 -0
- package/examples/rdf-surfaces-ancestor.n3 +6 -0
- package/examples/rdf-surfaces-city.n3 +6 -0
- package/examples/rdf-surfaces-domain.n3 +6 -0
- package/examples/rdf-surfaces-multi-premise.n3 +6 -0
- package/examples/rdf-surfaces-owl-all-values-from-codex.n3 +10 -0
- package/examples/rdf-surfaces-property-chain.n3 +6 -0
- package/examples/rdf-surfaces-range.n3 +6 -0
- package/examples/rdf-surfaces-rdfs-range-codex.n3 +6 -0
- package/examples/rdf-surfaces-rdfs-subclass-codex.n3 +6 -0
- package/eyeling.js +711 -21
- package/index.d.ts +4 -0
- package/index.js +2 -1
- package/lib/builtins.js +128 -3
- package/lib/cli.js +11 -4
- package/lib/engine.js +15 -7
- package/lib/lexer.js +4 -0
- package/lib/multisource.js +5 -3
- package/lib/prelude.js +18 -0
- package/lib/rdf_surfaces.js +524 -0
- package/lib/rules.js +3 -4
- package/package.json +8 -5
- package/test/builtins.test.js +36 -0
- package/test/examples.test.js +37 -5
- package/test/rdf_surfaces.test.js +204 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eyeling Reasoner — RDF Surfaces syntax normalizer
|
|
3
|
+
*
|
|
4
|
+
* Implements a small RDF Surfaces text convention inspired by Hayes' BLOGIC
|
|
5
|
+
* slides: `%not[ ... %]` surface parentheses with explicit blank mark binders
|
|
6
|
+
* such as `_:x _:y` at the beginning of a surface. The supported fragment
|
|
7
|
+
* covers slide 32, the slide 33 range shape, both slide 33 allValuesFrom
|
|
8
|
+
* shapes, and top-level fuse surfaces.
|
|
9
|
+
*
|
|
10
|
+
* The normalizer rewrites the supported fragment into ordinary Eyeling N3:
|
|
11
|
+
* %not[ _:x P(?x) . %not[ Q(?x) . %] %]
|
|
12
|
+
* becomes:
|
|
13
|
+
* { P(?x) . } => { Q(?x) . } .
|
|
14
|
+
*
|
|
15
|
+
* A top-level negative surface without an inner negative surface becomes an
|
|
16
|
+
* inference fuse:
|
|
17
|
+
* %not[ _:x P(?x) . %]
|
|
18
|
+
* becomes:
|
|
19
|
+
* { P(?x) . } => false .
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
function syntaxError(message, offset = null) {
|
|
25
|
+
const e = new Error(message);
|
|
26
|
+
e.name = 'N3SyntaxError';
|
|
27
|
+
if (typeof offset === 'number') e.offset = offset;
|
|
28
|
+
return e;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isWs(ch) {
|
|
32
|
+
return ch != null && /\s/.test(ch);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
function readStringAt(s, at) {
|
|
37
|
+
const quote = s[at];
|
|
38
|
+
let i = at;
|
|
39
|
+
let out = quote;
|
|
40
|
+
const long = s.startsWith(quote.repeat(3), i);
|
|
41
|
+
if (long) {
|
|
42
|
+
out = quote.repeat(3);
|
|
43
|
+
i += 3;
|
|
44
|
+
while (i < s.length) {
|
|
45
|
+
if (s.startsWith(quote.repeat(3), i)) {
|
|
46
|
+
out += quote.repeat(3);
|
|
47
|
+
i += 3;
|
|
48
|
+
return { text: out, end: i };
|
|
49
|
+
}
|
|
50
|
+
if (s[i] === '\\' && i + 1 < s.length) {
|
|
51
|
+
out += s.slice(i, i + 2);
|
|
52
|
+
i += 2;
|
|
53
|
+
} else {
|
|
54
|
+
out += s[i++];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw syntaxError('Unterminated string literal inside RDF Surface', at);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
i += 1;
|
|
61
|
+
let escaped = false;
|
|
62
|
+
while (i < s.length) {
|
|
63
|
+
const ch = s[i++];
|
|
64
|
+
out += ch;
|
|
65
|
+
if (escaped) escaped = false;
|
|
66
|
+
else if (ch === '\\') escaped = true;
|
|
67
|
+
else if (ch === quote) return { text: out, end: i };
|
|
68
|
+
}
|
|
69
|
+
throw syntaxError('Unterminated string literal inside RDF Surface', at);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readIriAt(s, at) {
|
|
73
|
+
let i = at + 1;
|
|
74
|
+
let out = '<';
|
|
75
|
+
while (i < s.length) {
|
|
76
|
+
const ch = s[i++];
|
|
77
|
+
out += ch;
|
|
78
|
+
if (ch === '>') return { text: out, end: i };
|
|
79
|
+
}
|
|
80
|
+
throw syntaxError('Unterminated IRI inside RDF Surface', at);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function skipWsAndComments(s, at) {
|
|
84
|
+
let i = at;
|
|
85
|
+
while (i < s.length) {
|
|
86
|
+
if (isWs(s[i])) {
|
|
87
|
+
i += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (s[i] === '#') {
|
|
91
|
+
while (i < s.length && s[i] !== '\n' && s[i] !== '\r') i += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
return i;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readBareTokenAt(s, at) {
|
|
100
|
+
const i0 = skipWsAndComments(s, at);
|
|
101
|
+
if (i0 >= s.length) return null;
|
|
102
|
+
if (s[i0] === '<') return readIriAt(s, i0);
|
|
103
|
+
if (s[i0] === '"' || s[i0] === "'") return readStringAt(s, i0);
|
|
104
|
+
let i = i0;
|
|
105
|
+
while (i < s.length && !isWs(s[i]) && !'{}[](),;.'.includes(s[i])) i += 1;
|
|
106
|
+
if (i === i0) return null;
|
|
107
|
+
return { text: s.slice(i0, i), start: i0, end: i };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function readStatementSegment(s) {
|
|
111
|
+
let i = 0;
|
|
112
|
+
let depthBrace = 0;
|
|
113
|
+
let depthBracket = 0;
|
|
114
|
+
let depthParen = 0;
|
|
115
|
+
while (i < s.length) {
|
|
116
|
+
if (s.startsWith('%not[', i) && depthBrace === 0 && depthBracket === 0 && depthParen === 0) {
|
|
117
|
+
return s.slice(0, i);
|
|
118
|
+
}
|
|
119
|
+
const ch = s[i];
|
|
120
|
+
if (ch === '"' || ch === "'") {
|
|
121
|
+
i = readStringAt(s, i).end;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === '<') {
|
|
125
|
+
i = readIriAt(s, i).end;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '#') {
|
|
129
|
+
while (i < s.length && s[i] !== '\n' && s[i] !== '\r') i += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (ch === '{') depthBrace += 1;
|
|
133
|
+
else if (ch === '}' && depthBrace > 0) depthBrace -= 1;
|
|
134
|
+
else if (ch === '[') depthBracket += 1;
|
|
135
|
+
else if (ch === ']' && depthBracket > 0) depthBracket -= 1;
|
|
136
|
+
else if (ch === '(') depthParen += 1;
|
|
137
|
+
else if (ch === ')' && depthParen > 0) depthParen -= 1;
|
|
138
|
+
else if (ch === '.' && depthBrace === 0 && depthBracket === 0 && depthParen === 0) return s.slice(0, i);
|
|
139
|
+
i += 1;
|
|
140
|
+
}
|
|
141
|
+
return s;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function tokenizeLeadingSegment(segment) {
|
|
145
|
+
const toks = [];
|
|
146
|
+
let pos = 0;
|
|
147
|
+
while (pos < segment.length) {
|
|
148
|
+
const tok = readBareTokenAt(segment, pos);
|
|
149
|
+
if (!tok) break;
|
|
150
|
+
toks.push(tok);
|
|
151
|
+
pos = tok.end;
|
|
152
|
+
}
|
|
153
|
+
return toks;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function extractLeadingBinders(raw) {
|
|
157
|
+
const text = String(raw || '');
|
|
158
|
+
const contentStart = skipWsAndComments(text, 0);
|
|
159
|
+
if (contentStart >= text.length) return { binders: [], text };
|
|
160
|
+
|
|
161
|
+
const segment = readStatementSegment(text.slice(contentStart));
|
|
162
|
+
const toks = tokenizeLeadingSegment(segment);
|
|
163
|
+
let leadingBlankCount = 0;
|
|
164
|
+
while (leadingBlankCount < toks.length && /^_:[A-Za-z_][A-Za-z0-9._-]*$/.test(toks[leadingBlankCount].text)) {
|
|
165
|
+
leadingBlankCount += 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (leadingBlankCount === 0) return { binders: [], text };
|
|
169
|
+
|
|
170
|
+
let binderCount = 0;
|
|
171
|
+
if (toks.length >= 3) {
|
|
172
|
+
// Prefer the longest explicit binder prefix that still leaves at least a
|
|
173
|
+
// subject, predicate, and object for the first statement. This matches the
|
|
174
|
+
// BLOGIC slide convention, e.g. `%not[ _:x _:x a :C . ... %]`.
|
|
175
|
+
binderCount = Math.min(leadingBlankCount, Math.max(0, toks.length - 3));
|
|
176
|
+
} else {
|
|
177
|
+
// No own triple before a nested surface: treat the leading marks as binders.
|
|
178
|
+
binderCount = leadingBlankCount;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (binderCount <= 0) return { binders: [], text };
|
|
182
|
+
|
|
183
|
+
const binders = toks.slice(0, binderCount).map((t) => t.text.slice(2));
|
|
184
|
+
const cut = toks[binderCount - 1].end;
|
|
185
|
+
return { binders, text: text.slice(0, contentStart) + text.slice(contentStart + cut) };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function splitTopLevelStatements(raw, surfaceOffset = null) {
|
|
189
|
+
const text = String(raw || '');
|
|
190
|
+
const out = [];
|
|
191
|
+
let start = 0;
|
|
192
|
+
let i = 0;
|
|
193
|
+
let depthBrace = 0;
|
|
194
|
+
let depthBracket = 0;
|
|
195
|
+
let depthParen = 0;
|
|
196
|
+
|
|
197
|
+
while (i < text.length) {
|
|
198
|
+
const ch = text[i];
|
|
199
|
+
if (ch === '"' || ch === "'") {
|
|
200
|
+
i = readStringAt(text, i).end;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (ch === '<') {
|
|
204
|
+
i = readIriAt(text, i).end;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (ch === '#') {
|
|
208
|
+
while (i < text.length && text[i] !== '\n' && text[i] !== '\r') i += 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (ch === '{') depthBrace += 1;
|
|
212
|
+
else if (ch === '}' && depthBrace > 0) depthBrace -= 1;
|
|
213
|
+
else if (ch === '[') depthBracket += 1;
|
|
214
|
+
else if (ch === ']' && depthBracket > 0) depthBracket -= 1;
|
|
215
|
+
else if (ch === '(') depthParen += 1;
|
|
216
|
+
else if (ch === ')' && depthParen > 0) depthParen -= 1;
|
|
217
|
+
else if (ch === '.' && depthBrace === 0 && depthBracket === 0 && depthParen === 0) {
|
|
218
|
+
const stmt = text.slice(start, i).trim();
|
|
219
|
+
if (stmt) out.push(stmt);
|
|
220
|
+
start = i + 1;
|
|
221
|
+
}
|
|
222
|
+
i += 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const tail = text.slice(start).trim();
|
|
226
|
+
if (tail) {
|
|
227
|
+
// A raw binder-only segment is OK; any other dangling text is most likely a
|
|
228
|
+
// missing dot in the surface body.
|
|
229
|
+
if (!/^_:[A-Za-z_][A-Za-z0-9._-]*(?:\s+_:[A-Za-z_][A-Za-z0-9._-]*)*$/.test(tail)) {
|
|
230
|
+
throw syntaxError('RDF Surface statement is missing a terminating dot', surfaceOffset);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readSurfaceAt(s, at) {
|
|
238
|
+
if (!s.startsWith('%not[', at)) return null;
|
|
239
|
+
let i = at + '%not['.length;
|
|
240
|
+
let current = '';
|
|
241
|
+
const segments = [];
|
|
242
|
+
const children = [];
|
|
243
|
+
|
|
244
|
+
while (i < s.length) {
|
|
245
|
+
if (s.startsWith('%]', i)) {
|
|
246
|
+
segments.push(current);
|
|
247
|
+
i += 2;
|
|
248
|
+
const raw = segments.join('\n');
|
|
249
|
+
const stripped = extractLeadingBinders(raw);
|
|
250
|
+
return {
|
|
251
|
+
type: 'not',
|
|
252
|
+
start: at,
|
|
253
|
+
end: i,
|
|
254
|
+
binders: stripped.binders,
|
|
255
|
+
statements: splitTopLevelStatements(stripped.text, at),
|
|
256
|
+
children,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (s.startsWith('%not[', i)) {
|
|
261
|
+
segments.push(current);
|
|
262
|
+
current = '';
|
|
263
|
+
const child = readSurfaceAt(s, i);
|
|
264
|
+
children.push(child);
|
|
265
|
+
i = child.end;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const ch = s[i];
|
|
270
|
+
if (ch === '"' || ch === "'") {
|
|
271
|
+
const str = readStringAt(s, i);
|
|
272
|
+
current += str.text;
|
|
273
|
+
i = str.end;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (ch === '<') {
|
|
277
|
+
const iri = readIriAt(s, i);
|
|
278
|
+
current += iri.text;
|
|
279
|
+
i = iri.end;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (ch === '#') {
|
|
283
|
+
while (i < s.length) {
|
|
284
|
+
const c = s[i++];
|
|
285
|
+
current += c;
|
|
286
|
+
if (c === '\n' || c === '\r') break;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
current += ch;
|
|
292
|
+
i += 1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw syntaxError('Unterminated RDF Surface, expected %]', at);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const LOG_FOR_ALL_IN_IRI = '<http://www.w3.org/2000/10/swap/log#forAllIn>';
|
|
299
|
+
|
|
300
|
+
function rewriteBlankMarksWithMap(statement, labelToVarName) {
|
|
301
|
+
const map = labelToVarName instanceof Map ? labelToVarName : new Map();
|
|
302
|
+
let out = '';
|
|
303
|
+
let i = 0;
|
|
304
|
+
while (i < statement.length) {
|
|
305
|
+
const ch = statement[i];
|
|
306
|
+
if (ch === '"' || ch === "'") {
|
|
307
|
+
const str = readStringAt(statement, i);
|
|
308
|
+
out += str.text;
|
|
309
|
+
i = str.end;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (ch === '<') {
|
|
313
|
+
const iri = readIriAt(statement, i);
|
|
314
|
+
out += iri.text;
|
|
315
|
+
i = iri.end;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (ch === '#') {
|
|
319
|
+
while (i < statement.length) {
|
|
320
|
+
const c = statement[i++];
|
|
321
|
+
out += c;
|
|
322
|
+
if (c === '\n' || c === '\r') break;
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (statement.startsWith('_:', i)) {
|
|
327
|
+
let j = i + 2;
|
|
328
|
+
while (j < statement.length && !isWs(statement[j]) && !'{}[](),;.'.includes(statement[j])) j += 1;
|
|
329
|
+
const label = statement.slice(i + 2, j);
|
|
330
|
+
const mapped = label ? map.get(label) : null;
|
|
331
|
+
if (mapped) {
|
|
332
|
+
out += `?${mapped}`;
|
|
333
|
+
i = j;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
out += ch;
|
|
338
|
+
i += 1;
|
|
339
|
+
}
|
|
340
|
+
return out.trim();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function formatGraphWithMap(statements, labelMap) {
|
|
344
|
+
const body = (statements || [])
|
|
345
|
+
.map((st) => rewriteBlankMarksWithMap(st, labelMap))
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
.map((st) => ` ${st} .`)
|
|
348
|
+
.join('\n');
|
|
349
|
+
return body ? `{
|
|
350
|
+
${body}\n}` : '{ }';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function formatGraphFromRewritten(statements) {
|
|
354
|
+
const body = (statements || [])
|
|
355
|
+
.map((st) => String(st || '').trim())
|
|
356
|
+
.filter(Boolean)
|
|
357
|
+
.map((st) => ` ${st} .`)
|
|
358
|
+
.join('\n');
|
|
359
|
+
return body ? `{
|
|
360
|
+
${body}\n}` : '{ }';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function makeVarMap(labels, prefix = '') {
|
|
364
|
+
const map = new Map();
|
|
365
|
+
for (const label of labels || []) map.set(label, `${prefix}${label}`);
|
|
366
|
+
return map;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function mergeVarMaps(...maps) {
|
|
370
|
+
const out = new Map();
|
|
371
|
+
for (const m of maps) {
|
|
372
|
+
for (const [k, v] of m.entries()) out.set(k, v);
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function slide33ReverseAllValuesFromRule(node, inheritedMap = new Map(), extraPremises = []) {
|
|
378
|
+
const outerBinders = node.binders || [];
|
|
379
|
+
const own = node.statements || [];
|
|
380
|
+
const children = node.children || [];
|
|
381
|
+
|
|
382
|
+
if (own.length !== 0 || outerBinders.length === 0 || children.length !== 2) return null;
|
|
383
|
+
|
|
384
|
+
let bodyChild = null;
|
|
385
|
+
let headChild = null;
|
|
386
|
+
for (const child of children) {
|
|
387
|
+
const childChildren = child && child.children ? child.children : [];
|
|
388
|
+
if (childChildren.length === 1) bodyChild = child;
|
|
389
|
+
else if (childChildren.length === 0) headChild = child;
|
|
390
|
+
else return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!bodyChild || !headChild) return null;
|
|
394
|
+
if (!bodyChild.statements || bodyChild.statements.length === 0) return null;
|
|
395
|
+
if (!headChild.statements || headChild.statements.length === 0) return null;
|
|
396
|
+
|
|
397
|
+
const thenChild = bodyChild.children[0];
|
|
398
|
+
if (!thenChild || (thenChild.children && thenChild.children.length)) return null;
|
|
399
|
+
if (!thenChild.statements || thenChild.statements.length === 0) return null;
|
|
400
|
+
|
|
401
|
+
const outerMap = mergeVarMaps(inheritedMap, makeVarMap(outerBinders));
|
|
402
|
+
const witnessMap = makeVarMap(bodyChild.binders || [], '__rs_witness_');
|
|
403
|
+
const localMap = makeVarMap(bodyChild.binders || [], '__rs_');
|
|
404
|
+
|
|
405
|
+
const mappedExtra = (extraPremises || [])
|
|
406
|
+
.map((st) => rewriteBlankMarksWithMap(st, outerMap))
|
|
407
|
+
.filter(Boolean);
|
|
408
|
+
const premiseStmts = bodyChild.statements
|
|
409
|
+
.map((st) => rewriteBlankMarksWithMap(st, mergeVarMaps(outerMap, witnessMap)))
|
|
410
|
+
.filter(Boolean);
|
|
411
|
+
|
|
412
|
+
const whereGraph = formatGraphWithMap(bodyChild.statements, mergeVarMaps(outerMap, localMap));
|
|
413
|
+
const thenGraph = formatGraphWithMap(thenChild.statements, mergeVarMaps(outerMap, localMap));
|
|
414
|
+
const forAllLine = `( ${whereGraph} ${thenGraph} ) ${LOG_FOR_ALL_IN_IRI} 1`;
|
|
415
|
+
|
|
416
|
+
const premise = formatGraphFromRewritten([...mappedExtra, ...premiseStmts, forAllLine]);
|
|
417
|
+
const conclusion = formatGraphWithMap(headChild.statements, outerMap);
|
|
418
|
+
return `${premise} => ${conclusion} .`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
function translateHeadSurface(node, extraPremises, inheritedMap = new Map()) {
|
|
423
|
+
const rules = [];
|
|
424
|
+
const map = inheritedMap instanceof Map ? inheritedMap : new Map();
|
|
425
|
+
const own = node.statements || [];
|
|
426
|
+
if (own.length) {
|
|
427
|
+
rules.push(`${formatGraphWithMap(extraPremises, map)} => ${formatGraphWithMap(own, map)} .`);
|
|
428
|
+
}
|
|
429
|
+
for (const child of node.children || []) {
|
|
430
|
+
rules.push(...translateRuleSurface(child, extraPremises, map));
|
|
431
|
+
}
|
|
432
|
+
return rules;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function translateRuleSurface(node, extraPremises = [], inheritedMap = new Map()) {
|
|
436
|
+
const slide33Reverse = slide33ReverseAllValuesFromRule(node, inheritedMap, extraPremises);
|
|
437
|
+
if (slide33Reverse) return [slide33Reverse];
|
|
438
|
+
|
|
439
|
+
const map = mergeVarMaps(inheritedMap, makeVarMap(node.binders || []));
|
|
440
|
+
const own = node.statements || [];
|
|
441
|
+
const premise = [...(extraPremises || []), ...own];
|
|
442
|
+
const children = node.children || [];
|
|
443
|
+
|
|
444
|
+
if (children.length === 0) {
|
|
445
|
+
return own.length ? [`${formatGraphWithMap(premise, map)} => false .`] : [];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return children.flatMap((child) => translateHeadSurface(child, premise, map));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function translateTopLevelSurface(node) {
|
|
452
|
+
const map = makeVarMap(node.binders || []);
|
|
453
|
+
const own = node.statements || [];
|
|
454
|
+
|
|
455
|
+
if (!node.children || node.children.length === 0) {
|
|
456
|
+
return own.length ? [`${formatGraphWithMap(own, map)} => false .`] : [];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const slide33Reverse = slide33ReverseAllValuesFromRule(node);
|
|
460
|
+
if (slide33Reverse) return [slide33Reverse];
|
|
461
|
+
|
|
462
|
+
return node.children.flatMap((child) => translateHeadSurface(child, own, map));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function normalizeRdfSurfaces(inputText) {
|
|
466
|
+
const s = String(inputText ?? '');
|
|
467
|
+
if (!s.includes('%not[')) return s;
|
|
468
|
+
|
|
469
|
+
let out = '';
|
|
470
|
+
const generated = [];
|
|
471
|
+
let i = 0;
|
|
472
|
+
let braceDepth = 0;
|
|
473
|
+
let bracketDepth = 0;
|
|
474
|
+
let parenDepth = 0;
|
|
475
|
+
|
|
476
|
+
while (i < s.length) {
|
|
477
|
+
if (s.startsWith('%not[', i) && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
|
|
478
|
+
const surface = readSurfaceAt(s, i);
|
|
479
|
+
generated.push(...translateTopLevelSurface(surface));
|
|
480
|
+
i = surface.end;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const ch = s[i];
|
|
485
|
+
if (ch === '"' || ch === "'") {
|
|
486
|
+
const str = readStringAt(s, i);
|
|
487
|
+
out += str.text;
|
|
488
|
+
i = str.end;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (ch === '<') {
|
|
492
|
+
const iri = readIriAt(s, i);
|
|
493
|
+
out += iri.text;
|
|
494
|
+
i = iri.end;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (ch === '#') {
|
|
498
|
+
while (i < s.length) {
|
|
499
|
+
const c = s[i++];
|
|
500
|
+
out += c;
|
|
501
|
+
if (c === '\n' || c === '\r') break;
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (ch === '{') braceDepth += 1;
|
|
507
|
+
else if (ch === '}' && braceDepth > 0) braceDepth -= 1;
|
|
508
|
+
else if (ch === '[') bracketDepth += 1;
|
|
509
|
+
else if (ch === ']' && bracketDepth > 0) bracketDepth -= 1;
|
|
510
|
+
else if (ch === '(') parenDepth += 1;
|
|
511
|
+
else if (ch === ')' && parenDepth > 0) parenDepth -= 1;
|
|
512
|
+
|
|
513
|
+
out += ch;
|
|
514
|
+
i += 1;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (generated.length === 0) return out;
|
|
518
|
+
const sep = out.trim() ? (out.endsWith('\n') ? '\n' : '\n\n') : '';
|
|
519
|
+
return out + sep + generated.join('\n\n') + '\n';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
module.exports = {
|
|
523
|
+
normalizeRdfSurfaces,
|
|
524
|
+
};
|
package/lib/rules.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
GraphTerm,
|
|
18
18
|
Triple,
|
|
19
19
|
copyQuotedGraphMetadata,
|
|
20
|
+
INTERNAL_BLANK_VAR_PREFIX,
|
|
20
21
|
} = require('./prelude');
|
|
21
22
|
|
|
22
23
|
function liftBlankRuleVars(premise, conclusion) {
|
|
@@ -32,10 +33,8 @@ function liftBlankRuleVars(premise, conclusion) {
|
|
|
32
33
|
// These variables are implementation details used to correlate one blank
|
|
33
34
|
// node across the lifted rule body. Their names must not be spellable by
|
|
34
35
|
// user input; otherwise a user variable such as ?_b1 can capture the
|
|
35
|
-
// lifted blank node and leak it into the rule head.
|
|
36
|
-
//
|
|
37
|
-
const INTERNAL_BLANK_VAR_PREFIX = '\uE000eyeling_b';
|
|
38
|
-
|
|
36
|
+
// lifted blank node and leak it into the rule head. The public custom
|
|
37
|
+
// builtin API maps these names back to ordinary-looking ?_bN variables.
|
|
39
38
|
function blankToVar(label) {
|
|
40
39
|
let name = mapping[label];
|
|
41
40
|
if (name === undefined) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeling",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "A minimal Notation3 (N3) reasoner in JavaScript.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -38,11 +38,14 @@
|
|
|
38
38
|
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"
|
|
41
|
+
"lint": "eslint .",
|
|
42
|
+
"lint:fix": "eslint . --fix",
|
|
42
43
|
"build": "node tools/bundle.js",
|
|
43
44
|
"test:packlist": "node test/packlist.test.js",
|
|
44
|
-
"test:api": "node test/api.test.js && node test/stream_messages.test.js",
|
|
45
|
+
"test:api": "node test/api.test.js && node test/stream_messages.test.js && node test/rdf_surfaces.test.js",
|
|
46
|
+
"test:rdf-surfaces": "node test/rdf_surfaces.test.js",
|
|
45
47
|
"test:builtins": "node test/builtins.test.js",
|
|
48
|
+
"test:store": "node test/store.test.js",
|
|
46
49
|
"test:examples": "node test/examples.test.js",
|
|
47
50
|
"test:examples:proof": "node test/examples.test.js --proof-only",
|
|
48
51
|
"test:stream-messages": "node test/stream_messages.test.js",
|
|
@@ -58,10 +61,10 @@
|
|
|
58
61
|
"test": "npm run test:api && npm run test:builtins && npm run test:store && npm run test:examples && npm run test:examples:proof && npm run test:manifest && npm run test:rdf12 && npm run test:playground",
|
|
59
62
|
"posttest": "npm run test:package",
|
|
60
63
|
"preversion": "npm test",
|
|
61
|
-
"postversion": "git push origin HEAD --follow-tags"
|
|
62
|
-
"test:store": "node test/store.test.js"
|
|
64
|
+
"postversion": "git push origin HEAD --follow-tags"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
67
|
+
"eslint": "^9.0.0",
|
|
65
68
|
"rdf-test-suite": "^2.1.4"
|
|
66
69
|
},
|
|
67
70
|
"exports": {
|
package/test/builtins.test.js
CHANGED
|
@@ -7,6 +7,7 @@ const { detail, failResult, info, pass } = require('./report');
|
|
|
7
7
|
const builtins = require('../lib/builtins');
|
|
8
8
|
require('../lib/engine');
|
|
9
9
|
const { reason } = require('../index');
|
|
10
|
+
const { reasonStream } = require('../lib/engine');
|
|
10
11
|
|
|
11
12
|
const expectedApiKeys = [
|
|
12
13
|
'registerBuiltin',
|
|
@@ -218,6 +219,41 @@ const cases = [
|
|
|
218
219
|
assert.match(out, /:canonical :midnightRollover "2027-01-01T00:00:00Z"\^\^xsd:dateTime \./);
|
|
219
220
|
},
|
|
220
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: 'custom builtin API hides internal blank-node variable prefix',
|
|
224
|
+
run() {
|
|
225
|
+
const iri = 'http://example.org/custom#format';
|
|
226
|
+
builtins.unregisterBuiltin(iri);
|
|
227
|
+
builtins.registerBuiltin(iri, ({ goal, subst, api }) => {
|
|
228
|
+
const formatted = api.termToN3(goal.s);
|
|
229
|
+
assert.doesNotMatch(formatted, /\uE000eyeling_b/);
|
|
230
|
+
assert.match(formatted, /\?_b1/);
|
|
231
|
+
const next = api.unifyTerm(goal.o, api.internLiteral(JSON.stringify(formatted)), subst);
|
|
232
|
+
return next === null ? [] : [next];
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const out = reasonStream(`
|
|
237
|
+
@prefix : <http://example.org/> .
|
|
238
|
+
@prefix cb: <http://example.org/custom#> .
|
|
239
|
+
|
|
240
|
+
{
|
|
241
|
+
{ [] a ?class } cb:format ?format .
|
|
242
|
+
}
|
|
243
|
+
=>
|
|
244
|
+
{
|
|
245
|
+
:result :is ?format .
|
|
246
|
+
} .
|
|
247
|
+
`, { proof: false, includeInputFactsInClosure: false }).closureN3;
|
|
248
|
+
assert.doesNotMatch(out, /\uE000eyeling_b/);
|
|
249
|
+
assert.match(out, /:result :is/);
|
|
250
|
+
assert.match(out, /\?_b1/);
|
|
251
|
+
assert.match(out, /\?class/);
|
|
252
|
+
} finally {
|
|
253
|
+
builtins.unregisterBuiltin(iri);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
},
|
|
221
257
|
|
|
222
258
|
];
|
|
223
259
|
|