eyeleng 1.0.6 → 1.0.7
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 +275 -18
- package/dist/browser/eyeleng.browser.js +123 -13
- package/eyeleng.js +123 -13
- package/package.json +6 -4
- package/playground.html +1 -1
- package/reports/w3c-rdf-earl.ttl +11707 -0
- package/reports/w3c-shacl12-rules-earl.ttl +1055 -1336
- package/src/parser.js +76 -2
- package/src/rdfManifest.js +13 -0
- package/src/shacl12RulesManifest.js +386 -0
- package/src/tokenizer.js +47 -11
- package/test/api.test.js +32 -0
- package/test/shacl12-rules.test.js +37 -215
- package/test/w3c-rdf.test.js +5 -2
- package/tools/w3c-rdf.js +20 -1
- package/tools/w3c-shacl12-rules.js +55 -0
- package/HANDBOOK.md +0 -1070
package/HANDBOOK.md
DELETED
|
@@ -1,1070 +0,0 @@
|
|
|
1
|
-
# Eyeleng Handbook
|
|
2
|
-
|
|
3
|
-
This handbook explains Eyeleng — short for **EYE Logic Engine** — both as JavaScript code and as a reasoning machine. It is written for a computer science student who knows basic programming, data structures, and logic, but may not yet know RDF, SHACL Rules, or forward-chaining reasoners.
|
|
4
|
-
|
|
5
|
-
The chapters are meant to be read linearly. Each chapter also stands on its own, so you can jump directly to the parser, the evaluator, dependency analysis, imports, or the command-line interface when you need that part.
|
|
6
|
-
|
|
7
|
-
## Contents
|
|
8
|
-
|
|
9
|
-
- [1. What Eyeleng Is](#1-what-eyeleng-is)
|
|
10
|
-
- [2. The Reasoning Model](#2-the-reasoning-model)
|
|
11
|
-
- [3. Project Layout](#3-project-layout)
|
|
12
|
-
- [4. Terms: The Atoms of the Machine](#4-terms-the-atoms-of-the-machine)
|
|
13
|
-
- [5. Tokenization](#5-tokenization)
|
|
14
|
-
- [6. Parsing a Rule Set](#6-parsing-a-rule-set)
|
|
15
|
-
- [7. Data, Heads, and Bodies](#7-data-heads-and-bodies)
|
|
16
|
-
- [8. Turtle-Style Abbreviations](#8-turtle-style-abbreviations)
|
|
17
|
-
- [9. Property Paths in Bodies](#9-property-paths-in-bodies)
|
|
18
|
-
- [10. Triple Storage and Matching](#10-triple-storage-and-matching)
|
|
19
|
-
- [11. Expressions and Builtins](#11-expressions-and-builtins)
|
|
20
|
-
- [12. Evaluating a Rule Body](#12-evaluating-a-rule-body)
|
|
21
|
-
- [13. Forward Chaining](#13-forward-chaining)
|
|
22
|
-
- [14. Dependency Analysis](#14-dependency-analysis)
|
|
23
|
-
- [15. Stratified Evaluation](#15-stratified-evaluation)
|
|
24
|
-
- [16. Assignment Rules](#16-assignment-rules)
|
|
25
|
-
- [17. Imports](#17-imports)
|
|
26
|
-
- [18. Static Diagnostics](#18-static-diagnostics)
|
|
27
|
-
- [19. Query as an Operation](#19-query-as-an-operation)
|
|
28
|
-
- [20. The CLI](#20-the-cli)
|
|
29
|
-
- [21. The Public API](#21-the-public-api)
|
|
30
|
-
- [22. The Bundle](#22-the-bundle)
|
|
31
|
-
- [23. Tests as Executable Documentation](#23-tests-as-executable-documentation)
|
|
32
|
-
- [24. Walking Through a Complete Run](#24-walking-through-a-complete-run)
|
|
33
|
-
- [25. W3C Draft Examples](#25-w3c-draft-examples)
|
|
34
|
-
- [26. Advanced SRL Grammar Features](#26-advanced-srl-grammar-features)
|
|
35
|
-
- [27. Reifiers, Triple Terms, and Annotations](#27-reifiers-triple-terms-and-annotations)
|
|
36
|
-
- [28. BuiltInCall Coverage](#28-builtincall-coverage)
|
|
37
|
-
- [29. RDF Rules Syntax Front-End](#29-rdf-rules-syntax-front-end)
|
|
38
|
-
- [30. Known Limitations](#30-known-limitations)
|
|
39
|
-
- [31. How to Extend Eyeleng Safely](#31-how-to-extend-eyeleng-safely)
|
|
40
|
-
- [32. The Big Picture](#32-the-big-picture)
|
|
41
|
-
|
|
42
|
-
## 1. What Eyeleng Is
|
|
43
|
-
|
|
44
|
-
Eyeleng stands for **EYE Logic Engine**. It is a compact JavaScript implementation of SHACL 1.2 Rules, including the Shape Rules Language, or SRL, and RDF Rules syntax front-ends.
|
|
45
|
-
|
|
46
|
-
A tiny Eyeleng program looks like this:
|
|
47
|
-
|
|
48
|
-
```srl
|
|
49
|
-
PREFIX : <http://example/>
|
|
50
|
-
|
|
51
|
-
DATA {
|
|
52
|
-
:Socrates a :Man .
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
RULE { ?x a :Mortal } WHERE { ?x a :Man }
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
The data says that Socrates is a man. The rule says that every man is mortal. Eyeleng applies the rule and derives:
|
|
59
|
-
|
|
60
|
-
```srl
|
|
61
|
-
:Socrates a :Mortal .
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Eyeleng is deliberately small. It has no runtime dependencies and avoids parser generators, RDF databases, and SPARQL engines. The point is that the whole reasoning pipeline can be read as ordinary JavaScript.
|
|
65
|
-
|
|
66
|
-
The main pieces are:
|
|
67
|
-
|
|
68
|
-
1. tokenization,
|
|
69
|
-
2. parsing,
|
|
70
|
-
3. RDF-like term representation,
|
|
71
|
-
4. triple storage and indexing,
|
|
72
|
-
5. pattern matching,
|
|
73
|
-
6. expression evaluation,
|
|
74
|
-
7. dependency analysis and stratification,
|
|
75
|
-
8. forward-chaining evaluation,
|
|
76
|
-
9. query-as-an-operation,
|
|
77
|
-
10. CLI output and bundling.
|
|
78
|
-
|
|
79
|
-
Eyeleng is not a standards conformance claim. It follows the SRL draft where it implements a feature, while staying honest about missing features.
|
|
80
|
-
|
|
81
|
-
## 2. The Reasoning Model
|
|
82
|
-
|
|
83
|
-
Eyeleng reasons over triples. A triple has this shape:
|
|
84
|
-
|
|
85
|
-
```text
|
|
86
|
-
subject predicate object
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
For example:
|
|
90
|
-
|
|
91
|
-
```srl
|
|
92
|
-
:alice :parentOf :bob .
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
means:
|
|
96
|
-
|
|
97
|
-
```text
|
|
98
|
-
subject = :alice
|
|
99
|
-
predicate = :parentOf
|
|
100
|
-
object = :bob
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
A rule has a body and a head:
|
|
104
|
-
|
|
105
|
-
```srl
|
|
106
|
-
RULE { head } WHERE { body }
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
The body is a pattern to match against known triples. The head is a template for new triples. When the body matches, variables in the head are replaced by values from the match.
|
|
110
|
-
|
|
111
|
-
For example:
|
|
112
|
-
|
|
113
|
-
```srl
|
|
114
|
-
RULE { ?child :childOf ?parent } WHERE { ?parent :parentOf ?child }
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
If the graph contains:
|
|
118
|
-
|
|
119
|
-
```srl
|
|
120
|
-
:alice :parentOf :bob .
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
then the body matches with:
|
|
124
|
-
|
|
125
|
-
```text
|
|
126
|
-
?parent = :alice
|
|
127
|
-
?child = :bob
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Substitution into the head creates:
|
|
131
|
-
|
|
132
|
-
```srl
|
|
133
|
-
:bob :childOf :alice .
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
The closure is the set of all facts known after all rules have fired until no new triples can be added.
|
|
137
|
-
|
|
138
|
-
## 3. Project Layout
|
|
139
|
-
|
|
140
|
-
The project is organized as small CommonJS modules:
|
|
141
|
-
|
|
142
|
-
```text
|
|
143
|
-
src/tokenizer.js source text -> tokens
|
|
144
|
-
src/parser.js tokens -> program object
|
|
145
|
-
src/term.js terms, keys, equality, formatting
|
|
146
|
-
src/store.js triple set, predicate index, matching, paths
|
|
147
|
-
src/builtins.js expression evaluation and built-in functions
|
|
148
|
-
src/analyze.js static diagnostics, dependencies, strata
|
|
149
|
-
src/engine.js layered forward-chaining evaluator
|
|
150
|
-
src/query.js external raw-body query operation
|
|
151
|
-
src/format.js text and JSON output
|
|
152
|
-
src/api.js public JavaScript API and import merging
|
|
153
|
-
src/cli.js command-line interface
|
|
154
|
-
tools/bundle.js self-contained bundle generator
|
|
155
|
-
test/*.test.js Node built-in test suite
|
|
156
|
-
examples/*.srl runnable SRL examples
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
A good reading order is:
|
|
160
|
-
|
|
161
|
-
1. `term.js`, because all other modules manipulate terms.
|
|
162
|
-
2. `tokenizer.js`, because it defines lexical units.
|
|
163
|
-
3. `parser.js`, because it builds the program object.
|
|
164
|
-
4. `store.js`, because matching is the core data operation.
|
|
165
|
-
5. `builtins.js`, because filters and assignments use expressions.
|
|
166
|
-
6. `analyze.js`, because rule dependencies control safe execution.
|
|
167
|
-
7. `engine.js`, because it performs inference.
|
|
168
|
-
8. `api.js` and `cli.js`, because they connect the machine to users.
|
|
169
|
-
|
|
170
|
-
## 4. Terms: The Atoms of the Machine
|
|
171
|
-
|
|
172
|
-
The file `src/term.js` defines the values that can appear in triples and bindings.
|
|
173
|
-
|
|
174
|
-
Eyeleng uses plain JavaScript objects:
|
|
175
|
-
|
|
176
|
-
```js
|
|
177
|
-
{ type: 'iri', value: 'http://example/alice' }
|
|
178
|
-
{ type: 'var', value: 'x' }
|
|
179
|
-
{ type: 'blank', value: 'b1' }
|
|
180
|
-
{ type: 'literal', value: 22, datatype: 'http://www.w3.org/2001/XMLSchema#integer', lang: null }
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
It also has a representation for triple terms:
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
{ type: 'triple', s, p, o }
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
The important operations are:
|
|
190
|
-
|
|
191
|
-
- `termKey(term)`, which creates a stable string key,
|
|
192
|
-
- `termEquals(a, b)`, which compares terms by key,
|
|
193
|
-
- `formatTerm(term, prefixes)`, which prints a compact SRL-like form,
|
|
194
|
-
- `valueToTerm(value)`, which turns JavaScript values into literal terms.
|
|
195
|
-
|
|
196
|
-
Reasoning depends on exact identity. Two IRIs match only if their expanded strings match. Two literals match only if their lexical value, datatype, and language tag match.
|
|
197
|
-
|
|
198
|
-
## 5. Tokenization
|
|
199
|
-
|
|
200
|
-
The tokenizer reads characters and emits tokens. A token is a record such as:
|
|
201
|
-
|
|
202
|
-
```js
|
|
203
|
-
{ type: 'word', value: 'RULE', line: 5, column: 1 }
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
The tokenizer recognizes:
|
|
207
|
-
|
|
208
|
-
- comments beginning with `#`,
|
|
209
|
-
- IRI references such as `<http://example/alice>`,
|
|
210
|
-
- single, double, and long string literals,
|
|
211
|
-
- variables such as `?x` and `$x`,
|
|
212
|
-
- numbers,
|
|
213
|
-
- punctuation such as `{`, `}`, `(`, `)`, `.`, `,`, and `;`,
|
|
214
|
-
- operators such as `:=`, `=`, `!=`, `<`, `<=`, `>=`, `&&`, `||`, `/`, and `^`,
|
|
215
|
-
- triple-term delimiters `<<(` and `)>>`.
|
|
216
|
-
|
|
217
|
-
The tokenizer does not know what a valid rule is. It only turns raw text into a stream of pieces. Grammar decisions are made by the parser.
|
|
218
|
-
|
|
219
|
-
Location data is preserved so syntax errors can point to useful line and column positions.
|
|
220
|
-
|
|
221
|
-
## 6. Parsing a Rule Set
|
|
222
|
-
|
|
223
|
-
The parser turns tokens into a program object:
|
|
224
|
-
|
|
225
|
-
```js
|
|
226
|
-
{
|
|
227
|
-
baseIRI,
|
|
228
|
-
version,
|
|
229
|
-
imports,
|
|
230
|
-
prefixes,
|
|
231
|
-
data,
|
|
232
|
-
rules
|
|
233
|
-
}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
The supported top-level SRL forms include:
|
|
237
|
-
|
|
238
|
-
```srl
|
|
239
|
-
BASE <http://example/base/>
|
|
240
|
-
PREFIX : <http://example/>
|
|
241
|
-
VERSION "1.2"
|
|
242
|
-
IMPORTS <other-rules.srl>
|
|
243
|
-
|
|
244
|
-
DATA { :alice :parentOf :bob . }
|
|
245
|
-
RULE { ?x :ancestorOf ?y } WHERE { ?x :parentOf ?y }
|
|
246
|
-
IF { ?x a :Man } THEN { ?x a :Mortal }
|
|
247
|
-
TRANSITIVE(:parentOf)
|
|
248
|
-
SYMMETRIC(:spouseOf)
|
|
249
|
-
INVERSE(:hasChild, :childOf)
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
The parser expands `TRANSITIVE`, `SYMMETRIC`, and `INVERSE` into ordinary rules. This keeps the evaluator simple: it evaluates rules, not special declarations.
|
|
253
|
-
|
|
254
|
-
The parser intentionally rejects optional rule-name syntax such as:
|
|
255
|
-
|
|
256
|
-
```srl
|
|
257
|
-
RULE :myRule { ... } WHERE { ... }
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
That form was useful in earlier prototypes, but it is not part of the SRL grammar used by Eyeleng now.
|
|
261
|
-
|
|
262
|
-
## 7. Data, Heads, and Bodies
|
|
263
|
-
|
|
264
|
-
A `DATA` block contributes input triples.
|
|
265
|
-
|
|
266
|
-
A rule head is a list of triple templates:
|
|
267
|
-
|
|
268
|
-
```srl
|
|
269
|
-
RULE {
|
|
270
|
-
?x :q ?y .
|
|
271
|
-
?x :seen true .
|
|
272
|
-
}
|
|
273
|
-
WHERE { ?x :p ?y }
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
A rule body is a list of clauses. Eyeleng uses these internal shapes:
|
|
277
|
-
|
|
278
|
-
```js
|
|
279
|
-
{ type: 'triple', triple }
|
|
280
|
-
{ type: 'path', triple }
|
|
281
|
-
{ type: 'filter', expr }
|
|
282
|
-
{ type: 'not', body }
|
|
283
|
-
{ type: 'set', variable: 'x', expr }
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
A normal triple body clause matches one triple. A path body clause matches a restricted property path. A filter keeps or rejects bindings. A `NOT` clause performs negation as failure. A `SET` clause computes a new variable.
|
|
287
|
-
|
|
288
|
-
## 8. Turtle-Style Abbreviations
|
|
289
|
-
|
|
290
|
-
SRL’s grammar uses Turtle-like predicate and object lists. Eyeleng supports examples such as:
|
|
291
|
-
|
|
292
|
-
```srl
|
|
293
|
-
DATA {
|
|
294
|
-
:alice :knows :bob, :carol ;
|
|
295
|
-
:score 9 .
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
This expands to three triples:
|
|
300
|
-
|
|
301
|
-
```srl
|
|
302
|
-
:alice :knows :bob .
|
|
303
|
-
:alice :knows :carol .
|
|
304
|
-
:alice :score 9 .
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
The same abbreviation works in rule heads and bodies:
|
|
308
|
-
|
|
309
|
-
```srl
|
|
310
|
-
RULE {
|
|
311
|
-
?friend :knownBy ?person ;
|
|
312
|
-
:scoredFor ?score .
|
|
313
|
-
}
|
|
314
|
-
WHERE {
|
|
315
|
-
?person :knows ?friend ;
|
|
316
|
-
:score ?score .
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
This syntax makes examples shorter, but internally Eyeleng still stores plain triples.
|
|
321
|
-
|
|
322
|
-
## 9. Property Paths in Bodies
|
|
323
|
-
|
|
324
|
-
The SRL grammar includes restricted property paths. Eyeleng supports sequence `/` and inverse `^` paths in rule bodies.
|
|
325
|
-
|
|
326
|
-
Example:
|
|
327
|
-
|
|
328
|
-
```srl
|
|
329
|
-
RULE { ?x :grandparentOf ?z }
|
|
330
|
-
WHERE { ?x :parentOf/:parentOf ?z }
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
This means: find a path from `?x` to `?z` through two `:parentOf` edges.
|
|
334
|
-
|
|
335
|
-
Inverse paths flip direction:
|
|
336
|
-
|
|
337
|
-
```srl
|
|
338
|
-
RULE { ?child :hasParent ?parent }
|
|
339
|
-
WHERE { ?child ^:parentOf ?parent }
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
This matches when `?parent :parentOf ?child` exists.
|
|
343
|
-
|
|
344
|
-
Internally, `store.js` computes path pairs and then matches the subject and object against the current binding. The implementation is intentionally simple and finite. It does not implement arbitrary-length `*` or `+` paths.
|
|
345
|
-
|
|
346
|
-
## 10. Triple Storage and Matching
|
|
347
|
-
|
|
348
|
-
The triple store in `src/store.js` has two jobs:
|
|
349
|
-
|
|
350
|
-
1. remember which triples already exist,
|
|
351
|
-
2. find triples that match a pattern.
|
|
352
|
-
|
|
353
|
-
It uses a map from stable triple keys to triples. This prevents duplicates and lets the engine ask, “Is this inferred triple new?”
|
|
354
|
-
|
|
355
|
-
It also keeps indexes by predicate, by predicate plus subject, and by predicate plus object. When the pattern is:
|
|
356
|
-
|
|
357
|
-
```srl
|
|
358
|
-
?x :parentOf ?y
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Eyeleng only scans triples whose predicate is `:parentOf`. If the subject or object is already fixed by the pattern or by previous bindings, the more specific index is used. This is much cheaper than scanning the whole graph for every pattern and is especially important for type-heavy rule sets.
|
|
362
|
-
|
|
363
|
-
Matching a pattern against a triple either fails or produces a binding. Pattern:
|
|
364
|
-
|
|
365
|
-
```srl
|
|
366
|
-
?x :parentOf ?y
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
Triple:
|
|
370
|
-
|
|
371
|
-
```srl
|
|
372
|
-
:alice :parentOf :bob
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
Binding:
|
|
376
|
-
|
|
377
|
-
```js
|
|
378
|
-
{
|
|
379
|
-
x: { type: 'iri', value: 'http://example/alice' },
|
|
380
|
-
y: { type: 'iri', value: 'http://example/bob' }
|
|
381
|
-
}
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
A later clause must be compatible with the existing binding. This is the same idea as joining rows in a database query.
|
|
385
|
-
|
|
386
|
-
## 11. Expressions and Builtins
|
|
387
|
-
|
|
388
|
-
Filters and assignments use expressions:
|
|
389
|
-
|
|
390
|
-
```srl
|
|
391
|
-
FILTER(?age >= 18 && LANG(?name) = "en")
|
|
392
|
-
SET(?slug := REPLACE(LCASE(STR(?name)), " ", "-"))
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
Expressions are parsed into trees and evaluated by `src/builtins.js`.
|
|
396
|
-
|
|
397
|
-
Supported expression features include:
|
|
398
|
-
|
|
399
|
-
- arithmetic: `+`, `-`, `*`, `/`,
|
|
400
|
-
- comparisons: `=`, `!=`, `<`, `<=`, `>`, `>=`,
|
|
401
|
-
- logic: `&&`, `||`, `!`,
|
|
402
|
-
- membership: `IN` and `NOT IN`,
|
|
403
|
-
- strings: `CONCAT`, `SUBSTR`, `STRLEN`, `REPLACE`, `UCASE`, `LCASE`, `CONTAINS`, `STRSTARTS`, `STRENDS`, `STRBEFORE`, `STRAFTER`,
|
|
404
|
-
- term functions: `STR`, `IRI`, `URI`, `BNODE`, `DATATYPE`, `LANG`, `STRDT`, `STRLANG`,
|
|
405
|
-
- tests: `sameTerm`, `isIRI`, `isURI`, `isBLANK`, `isLITERAL`, `isNUMERIC`, `hasLANG`, `REGEX`,
|
|
406
|
-
- triple-term helpers: `TRIPLE`, `SUBJECT`, `PREDICATE`, `OBJECT`,
|
|
407
|
-
- deterministic process helpers for this implementation: `NOW`, `UUID`, `STRUUID`.
|
|
408
|
-
|
|
409
|
-
This is not a full SPARQL expression engine. It is a practical subset plus a few functions from the grammar that are useful for examples and tests.
|
|
410
|
-
|
|
411
|
-
## 12. Evaluating a Rule Body
|
|
412
|
-
|
|
413
|
-
`evaluateBody` in `src/engine.js` evaluates a list of clauses from left to right.
|
|
414
|
-
|
|
415
|
-
It starts with one empty binding:
|
|
416
|
-
|
|
417
|
-
```js
|
|
418
|
-
[{}]
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Each clause transforms the current bindings:
|
|
422
|
-
|
|
423
|
-
```text
|
|
424
|
-
bindings = [{}]
|
|
425
|
-
for clause in body:
|
|
426
|
-
bindings = apply(clause, bindings)
|
|
427
|
-
return bindings
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
For a triple or path clause, the store returns compatible matches.
|
|
431
|
-
|
|
432
|
-
For a filter, the expression must evaluate to true.
|
|
433
|
-
|
|
434
|
-
For `SET`, the expression result is converted to a term and assigned to a variable.
|
|
435
|
-
|
|
436
|
-
For `NOT`, Eyeleng tries to evaluate the inner body. If the inner body has no matches, the outer binding survives.
|
|
437
|
-
|
|
438
|
-
The order of clauses can matter for safety and performance. A filter or assignment should normally use variables that have already been bound by previous positive clauses.
|
|
439
|
-
|
|
440
|
-
## 13. Forward Chaining
|
|
441
|
-
|
|
442
|
-
The core inference loop is forward chaining. Conceptually, a recursive stratum is evaluated like this:
|
|
443
|
-
|
|
444
|
-
```text
|
|
445
|
-
store = base data + DATA blocks
|
|
446
|
-
repeat:
|
|
447
|
-
added = 0
|
|
448
|
-
for each rule in the recursive stratum:
|
|
449
|
-
bindings = evaluate body against store
|
|
450
|
-
for each binding:
|
|
451
|
-
instantiate head triples
|
|
452
|
-
add new triples to store
|
|
453
|
-
until added == 0
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
A fixpoint is reached when a full pass over the recursive stratum adds no new triples.
|
|
457
|
-
|
|
458
|
-
Acyclic strata are different. After lower strata have reached their fixpoints, each acyclic rule component only needs one pass. This matters for deep taxonomies: a chain of thousands of non-recursive classification rules should not consume recursive-fixpoint attempts and should not be mistaken for non-termination.
|
|
459
|
-
|
|
460
|
-
Recursive rules are allowed when they are monotonic. For example:
|
|
461
|
-
|
|
462
|
-
```srl
|
|
463
|
-
RULE { ?x :ancestorOf ?y } WHERE { ?x :parentOf ?y }
|
|
464
|
-
RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
These rules compute ancestry. If the input graph is finite and the rules do not create endlessly new terms, the closure is finite.
|
|
468
|
-
|
|
469
|
-
`--max-iterations` is a safety guard for recursive strata that do not terminate. It is applied within a recursive layer, not across the total number of acyclic dependency layers.
|
|
470
|
-
|
|
471
|
-
## 14. Dependency Analysis
|
|
472
|
-
|
|
473
|
-
Negation and assignment make rule order important. Eyeleng therefore analyzes dependencies before evaluation.
|
|
474
|
-
|
|
475
|
-
A rule depends on another rule when a body triple pattern can possibly be generated by a head template of the other rule. The analyzer indexes head templates before comparing them, using fixed subject, predicate, and object positions when available. Without that index, a deep taxonomy with thousands of rules would spend most of its time comparing every rule body with every rule head.
|
|
476
|
-
|
|
477
|
-
The evaluator also precomputes which dependency layers are genuinely recursive. A deep taxonomy can have tens of thousands of acyclic layers, and checking each layer by scanning every dependency edge would turn a linear benchmark into a quadratic one. Eyeleng therefore computes layer recursion once and then evaluates each acyclic layer in a single pass.
|
|
478
|
-
|
|
479
|
-
A positive dependency looks like this:
|
|
480
|
-
|
|
481
|
-
```srl
|
|
482
|
-
RULE { ?x :q ?y } WHERE { ?x :p ?y }
|
|
483
|
-
RULE { ?x :r ?y } WHERE { ?x :q ?y }
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
The second rule depends positively on the first.
|
|
487
|
-
|
|
488
|
-
A negative dependency looks like this:
|
|
489
|
-
|
|
490
|
-
```srl
|
|
491
|
-
RULE { ?x :ok true } WHERE { ?x a :Thing . NOT { ?x :bad true } }
|
|
492
|
-
RULE { ?x :bad true } WHERE { ?x :flagged true }
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
The first rule depends negatively on the second, because the meaning of `NOT { ?x :bad true }` is only stable after all possible `:bad` triples have been derived.
|
|
496
|
-
|
|
497
|
-
`src/analyze.js` builds this dependency graph. The CLI can print it with:
|
|
498
|
-
|
|
499
|
-
```sh
|
|
500
|
-
./eyeleng.js --check --deps examples/stratified-negation.srl
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
## 15. Stratified Evaluation
|
|
504
|
-
|
|
505
|
-
Stratification turns the dependency graph into evaluation layers.
|
|
506
|
-
|
|
507
|
-
If rule A has `NOT` over a predicate that rule B can produce, then B must finish before A runs. Otherwise A could infer something based on missing information and never retract it.
|
|
508
|
-
|
|
509
|
-
Eyeleng’s evaluator now uses layers from the dependency analysis. Each layer is evaluated to a fixpoint before moving to the next layer.
|
|
510
|
-
|
|
511
|
-
This matters when source order is misleading:
|
|
512
|
-
|
|
513
|
-
```srl
|
|
514
|
-
RULE { ?x :eligible true }
|
|
515
|
-
WHERE { ?x a :Person . NOT { ?x :blocked true } }
|
|
516
|
-
|
|
517
|
-
RULE { ?x :blocked true }
|
|
518
|
-
WHERE { ?x :flagged true }
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
Even though the `:eligible` rule appears first, Eyeleng runs the `:blocked` producer in an earlier layer. That prevents incorrect `:eligible` triples.
|
|
522
|
-
|
|
523
|
-
If a dependency cycle contains a negative edge, Eyeleng reports `unstratified-negation` and refuses to evaluate by default.
|
|
524
|
-
|
|
525
|
-
## 16. Assignment Rules
|
|
526
|
-
|
|
527
|
-
`SET` assigns the result of an expression to a variable:
|
|
528
|
-
|
|
529
|
-
```srl
|
|
530
|
-
RULE { ?x :distanceKm ?km }
|
|
531
|
-
WHERE {
|
|
532
|
-
?x :distanceMiles ?miles .
|
|
533
|
-
SET(?km := ?miles * 1.60934)
|
|
534
|
-
}
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
Eyeleng marks rules containing `SET` as run-once rules. They are evaluated at their layer position after ordinary rules in that layer have reached a fixpoint.
|
|
538
|
-
|
|
539
|
-
Deterministic assignment rules can participate in the ordinary fixpoint loop. Rules containing volatile generators such as UUID(), STRUUID(), or argument-less BNODE() are evaluated once so they do not repeatedly create different values for the same conceptual assignment.
|
|
540
|
-
|
|
541
|
-
If a `SET` rule is recursive, the analyzer warns because recursive assignment is often a sign that the program can be non-obvious or non-terminating.
|
|
542
|
-
|
|
543
|
-
## 17. Imports
|
|
544
|
-
|
|
545
|
-
The parser records `IMPORTS` declarations:
|
|
546
|
-
|
|
547
|
-
```srl
|
|
548
|
-
IMPORTS <library.srl>
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
The API can merge imported rule sets when you provide an `importResolver`. The CLI has a built-in resolver for local `file:` imports. When you run:
|
|
552
|
-
|
|
553
|
-
```sh
|
|
554
|
-
./eyeleng.js examples/import-main.srl
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
and `import-main.srl` contains:
|
|
558
|
-
|
|
559
|
-
```srl
|
|
560
|
-
IMPORTS <import-lib.srl>
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
Eyeleng resolves the relative IRI against the importing file, reads the imported file, recursively processes its imports, and merges imported rules and data before local evaluation.
|
|
564
|
-
|
|
565
|
-
Import cycles are tracked with a visited set, so a cycle does not cause infinite loading.
|
|
566
|
-
|
|
567
|
-
The self-contained CLI intentionally does not fetch remote HTTP imports. API users can provide their own resolver for that.
|
|
568
|
-
|
|
569
|
-
## 18. Static Diagnostics
|
|
570
|
-
|
|
571
|
-
Static analysis reports warnings and errors before evaluation.
|
|
572
|
-
|
|
573
|
-
Warnings include:
|
|
574
|
-
|
|
575
|
-
- a head variable is not bound by the body,
|
|
576
|
-
- a filter may use an unbound variable,
|
|
577
|
-
- a `SET` expression may use an unbound variable,
|
|
578
|
-
- a variable appears only inside `NOT`,
|
|
579
|
-
- a recursive assignment rule was detected.
|
|
580
|
-
|
|
581
|
-
Errors include:
|
|
582
|
-
|
|
583
|
-
- unstratified negation,
|
|
584
|
-
- invalid non-IRI/non-variable head predicates.
|
|
585
|
-
|
|
586
|
-
By default, warnings do not stop execution. With `--strict`, warnings become fatal. Errors stop execution unless an internal caller explicitly opts out.
|
|
587
|
-
|
|
588
|
-
Static analysis prevents the engine from silently doing something meaningless. For example:
|
|
589
|
-
|
|
590
|
-
```srl
|
|
591
|
-
RULE { ?x :bad true } WHERE { :alice :knows :bob }
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
The body never binds `?x`, so the head cannot be instantiated. Eyeleng warns about that.
|
|
595
|
-
|
|
596
|
-
## 19. Query as an Operation
|
|
597
|
-
|
|
598
|
-
The SHACL Rules draft describes a query operation, but Eyeleng does not add top-level `QUERY` or SPARQL `SELECT` syntax to `.srl` files.
|
|
599
|
-
|
|
600
|
-
Instead, query mode is external:
|
|
601
|
-
|
|
602
|
-
```sh
|
|
603
|
-
./eyeleng.js --query '?person :ancestorOf ?descendant . FILTER(?person = :alice)' examples/query.srl
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
The steps are:
|
|
607
|
-
|
|
608
|
-
1. parse and compile the rule set,
|
|
609
|
-
2. process imports,
|
|
610
|
-
3. compute the closure,
|
|
611
|
-
4. parse the query text as a raw SRL body pattern,
|
|
612
|
-
5. evaluate that pattern against the closure,
|
|
613
|
-
6. print bindings.
|
|
614
|
-
|
|
615
|
-
This keeps `.srl` files focused on SRL rule sets while still making the query operation available from the CLI and API.
|
|
616
|
-
|
|
617
|
-
## 20. The CLI
|
|
618
|
-
|
|
619
|
-
`src/cli.js` is a thin wrapper around the API.
|
|
620
|
-
|
|
621
|
-
Common commands:
|
|
622
|
-
|
|
623
|
-
```sh
|
|
624
|
-
./eyeleng.js examples/family.srl
|
|
625
|
-
./eyeleng.js --all examples/family.srl
|
|
626
|
-
./eyeleng.js --check --deps examples/stratified-negation.srl
|
|
627
|
-
./eyeleng.js --json --trace --stats examples/if-then.srl
|
|
628
|
-
./eyeleng.js --query-file examples/query-body.txt examples/query.srl
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
Important options:
|
|
632
|
-
|
|
633
|
-
```text
|
|
634
|
-
--check parse and analyze only
|
|
635
|
-
--strict warnings become fatal
|
|
636
|
-
--deps print dependency edges and layers
|
|
637
|
-
--trace show rule firings
|
|
638
|
-
--stats show iteration and rule counts
|
|
639
|
-
--json structured output
|
|
640
|
-
--max-iterations N
|
|
641
|
-
recursive-layer fixpoint safety guard
|
|
642
|
-
--query run a raw body pattern over the closure
|
|
643
|
-
--no-imports parse imports but do not load them
|
|
644
|
-
```
|
|
645
|
-
|
|
646
|
-
The CLI should remain boring. The core logic lives in `src/api.js`, `src/analyze.js`, and `src/engine.js` so tests and other programs can use the same behavior without spawning a process.
|
|
647
|
-
|
|
648
|
-
## 21. The Public API
|
|
649
|
-
|
|
650
|
-
Typical API use:
|
|
651
|
-
|
|
652
|
-
```js
|
|
653
|
-
const { run, formatTriples } = require('./src/index.js');
|
|
654
|
-
|
|
655
|
-
const result = run(`
|
|
656
|
-
PREFIX : <http://example/>
|
|
657
|
-
DATA { :Socrates a :Man . }
|
|
658
|
-
RULE { ?x a :Mortal } WHERE { ?x a :Man }
|
|
659
|
-
`);
|
|
660
|
-
|
|
661
|
-
console.log(formatTriples(result.inferred, result.prefixes));
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
For query mode:
|
|
665
|
-
|
|
666
|
-
```js
|
|
667
|
-
const { runQuery, formatBindings } = require('./src/index.js');
|
|
668
|
-
|
|
669
|
-
const result = runQuery(source, '?x :ancestorOf ?y');
|
|
670
|
-
console.log(formatBindings(result.query.bindings, result.prefixes));
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
For imports:
|
|
674
|
-
|
|
675
|
-
```js
|
|
676
|
-
const result = run(source, {
|
|
677
|
-
baseIRI: 'file:///main.srl',
|
|
678
|
-
importResolver(target) {
|
|
679
|
-
return {
|
|
680
|
-
source: readSomehow(target),
|
|
681
|
-
options: { baseIRI: target, filename: target }
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
The public API returns structured objects: parsed programs, diagnostics, inferred triples, closure triples, traces, stats, and query bindings.
|
|
688
|
-
|
|
689
|
-
## 22. The Bundle
|
|
690
|
-
|
|
691
|
-
`tools/bundle.js` generates `eyeleng.js`.
|
|
692
|
-
|
|
693
|
-
The bundler starts at `src/cli.js`, follows local `require('./...')` calls, places modules into a small runtime table, and writes one executable file.
|
|
694
|
-
|
|
695
|
-
The generated file starts with:
|
|
696
|
-
|
|
697
|
-
```text
|
|
698
|
-
#!/usr/bin/env node
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
This lets users run it directly on Unix-like systems.
|
|
702
|
-
|
|
703
|
-
The source modules remain the real implementation. The bundle is a distribution artifact. Run:
|
|
704
|
-
|
|
705
|
-
```sh
|
|
706
|
-
npm run bundle
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
or simply:
|
|
710
|
-
|
|
711
|
-
```sh
|
|
712
|
-
npm test
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
because the test script rebuilds the bundle first.
|
|
716
|
-
|
|
717
|
-
## 23. Tests as Executable Documentation
|
|
718
|
-
|
|
719
|
-
The tests use Node’s built-in `node:test` module.
|
|
720
|
-
|
|
721
|
-
`test/api.test.js` covers core behavior:
|
|
722
|
-
|
|
723
|
-
- parsing,
|
|
724
|
-
- recursion,
|
|
725
|
-
- filters,
|
|
726
|
-
- negation,
|
|
727
|
-
- assignment,
|
|
728
|
-
- typed and language literals,
|
|
729
|
-
- `VERSION`, blank nodes, and string forms,
|
|
730
|
-
- `IN` and `NOT IN`,
|
|
731
|
-
- property paths,
|
|
732
|
-
- stratified negation,
|
|
733
|
-
- imports through the API,
|
|
734
|
-
- rejection of non-SRL syntax.
|
|
735
|
-
|
|
736
|
-
`test/examples.test.js` runs real examples and CLI commands:
|
|
737
|
-
|
|
738
|
-
- `family.srl`,
|
|
739
|
-
- `negation.srl`,
|
|
740
|
-
- `assignment.srl`,
|
|
741
|
-
- `if-then.srl`,
|
|
742
|
-
- `declarations.srl`,
|
|
743
|
-
- `property-paths.srl`,
|
|
744
|
-
- `version-and-in.srl`,
|
|
745
|
-
- `stratified-negation.srl`,
|
|
746
|
-
- `import-main.srl`,
|
|
747
|
-
- query and diagnostics examples.
|
|
748
|
-
|
|
749
|
-
`test/deep-taxonomy.test.js` covers generated taxonomy benchmarks at multiple depths. The small examples are useful for reading. The larger examples guard against accidental quadratic behavior in dependency analysis or evaluation.
|
|
750
|
-
|
|
751
|
-
Tests are both regression checks and a compact specification of current behavior.
|
|
752
|
-
|
|
753
|
-
## 24. Walking Through a Complete Run
|
|
754
|
-
|
|
755
|
-
Consider:
|
|
756
|
-
|
|
757
|
-
```srl
|
|
758
|
-
PREFIX : <http://example/>
|
|
759
|
-
|
|
760
|
-
DATA {
|
|
761
|
-
:alice :parentOf :bob .
|
|
762
|
-
:bob :parentOf :carol .
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
RULE { ?x :ancestorOf ?y } WHERE { ?x :parentOf ?y }
|
|
766
|
-
RULE { ?x :ancestorOf ?z } WHERE { ?x :parentOf ?y . ?y :ancestorOf ?z }
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
The tokenizer emits tokens such as `PREFIX`, `DATA`, `:alice`, `RULE`, and `?x`.
|
|
770
|
-
|
|
771
|
-
The parser creates two data triples and two rules.
|
|
772
|
-
|
|
773
|
-
The analyzer sees positive recursion through `:ancestorOf`, which is allowed. It places the recursive rules into a layer.
|
|
774
|
-
|
|
775
|
-
The engine loads the `:parentOf` triples into the store.
|
|
776
|
-
|
|
777
|
-
In the first fixpoint pass, the base rule derives:
|
|
778
|
-
|
|
779
|
-
```srl
|
|
780
|
-
:alice :ancestorOf :bob .
|
|
781
|
-
:bob :ancestorOf :carol .
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
The recursive rule then derives:
|
|
785
|
-
|
|
786
|
-
```srl
|
|
787
|
-
:alice :ancestorOf :carol .
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
The next pass adds nothing. The layer has reached a fixpoint, and the program is finished.
|
|
791
|
-
|
|
792
|
-
## 25. W3C Draft Examples
|
|
793
|
-
|
|
794
|
-
The `examples/spec-*` files mirror examples from the SHACL 1.2 Rules Working Draft. These files serve two purposes.
|
|
795
|
-
|
|
796
|
-
First, the `.srl` files are executable regression examples. They cover the introductory rule examples: basic inference, recursion, filtering, negation, assignment, assignment guarded by negation, and the SRL syntax comparison example from section 4.
|
|
797
|
-
|
|
798
|
-
Second, the `.ttl` files preserve Turtle/RDF examples from the draft. Eyeleng can execute the RDF Rules syntax subset that maps `srl:RuleSet`, `srl:data`, `srl:rules`, `srl:body`, and `srl:head` into the same internal rule-set model used by SRL.
|
|
799
|
-
|
|
800
|
-
When adding future draft examples, keep this distinction clear:
|
|
801
|
-
|
|
802
|
-
```text
|
|
803
|
-
examples/spec-*.srl executable SRL examples
|
|
804
|
-
examples/spec-*.ttl captured RDF/Turtle sketches from the draft
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
The test file `test/w3c-examples.test.js` checks that all runnable W3C `.srl` examples execute through both the API and the bundled CLI.
|
|
808
|
-
|
|
809
|
-
## 26. Advanced SRL Grammar Features
|
|
810
|
-
|
|
811
|
-
The SRL grammar is not only `RULE`, `WHERE`, and simple triples. Eyeleng also supports several RDF-style term forms that make rule sets closer to the draft grammar.
|
|
812
|
-
|
|
813
|
-
Collections are written with parentheses:
|
|
814
|
-
|
|
815
|
-
```srl
|
|
816
|
-
DATA { :list :items ( :a :b :c ) . }
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
Internally, the parser expands the collection into RDF list triples using `rdf:first`, `rdf:rest`, and `rdf:nil`. The original compact syntax is only surface syntax; the reasoner still works over triples.
|
|
820
|
-
|
|
821
|
-
Blank-node property lists are written with square brackets:
|
|
822
|
-
|
|
823
|
-
```srl
|
|
824
|
-
DATA { :alice :knows [ :name "Bob" ; :age 22 ] . }
|
|
825
|
-
```
|
|
826
|
-
|
|
827
|
-
The parser creates a fresh blank node, asserts the surrounding triple, and asserts the property-list triples about that blank node. Anonymous blank nodes can also be written as `[]`.
|
|
828
|
-
|
|
829
|
-
Variables may use either `?x` or `$x`. Both spellings create the same internal variable representation. Supporting both matters because the SRL grammar permits both forms.
|
|
830
|
-
|
|
831
|
-
Language-direction literals preserve both the language and the direction:
|
|
832
|
-
|
|
833
|
-
```srl
|
|
834
|
-
DATA { :message :text "bonjour"@fr--ltr . }
|
|
835
|
-
```
|
|
836
|
-
|
|
837
|
-
The `LANG`, `LANGDIR`, and related builtins can inspect those fields.
|
|
838
|
-
|
|
839
|
-
Signed numeric literals and Unicode string escapes are parsed as RDF terms. Examples such as `-12`, `-3.5`, `"A\u0042"`, and `"A\U00000042"` work in data, rule heads, and expressions.
|
|
840
|
-
|
|
841
|
-
Triple terms use the parenthesized spelling:
|
|
842
|
-
|
|
843
|
-
```srl
|
|
844
|
-
<<( :alice :says :hello )>>
|
|
845
|
-
```
|
|
846
|
-
|
|
847
|
-
Triple terms can appear in data and patterns. Matching is recursive, so a pattern such as `<<(?s :p ?o)>>` can bind variables inside the triple term.
|
|
848
|
-
|
|
849
|
-
## 27. Reifiers, Triple Terms, and Annotations
|
|
850
|
-
|
|
851
|
-
Eyeleng distinguishes triple terms from reifiers.
|
|
852
|
-
|
|
853
|
-
A triple term is a term whose value is a triple:
|
|
854
|
-
|
|
855
|
-
```srl
|
|
856
|
-
<<( :alice :says :hello )>>
|
|
857
|
-
```
|
|
858
|
-
|
|
859
|
-
A reified triple can introduce a reifier:
|
|
860
|
-
|
|
861
|
-
```srl
|
|
862
|
-
<< :alice :says :hello ~ :claim1 >>
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
Eyeleng expands that into a reifier relationship:
|
|
866
|
-
|
|
867
|
-
```srl
|
|
868
|
-
:claim1 rdf:reifies <<( :alice :says :hello )>> .
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
Annotation blocks use the same model. This input:
|
|
872
|
-
|
|
873
|
-
```srl
|
|
874
|
-
:alice :says :hello ~ :claim1 {| :source :chat |} .
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
asserts the base triple and also creates metadata about `:claim1`:
|
|
878
|
-
|
|
879
|
-
```srl
|
|
880
|
-
:alice :says :hello .
|
|
881
|
-
:claim1 rdf:reifies <<( :alice :says :hello )>> .
|
|
882
|
-
:claim1 :source :chat .
|
|
883
|
-
```
|
|
884
|
-
|
|
885
|
-
That makes rule bodies explicit and readable:
|
|
886
|
-
|
|
887
|
-
```srl
|
|
888
|
-
RULE { ?speaker :statementSource ?source }
|
|
889
|
-
WHERE {
|
|
890
|
-
?claim rdf:reifies <<(?speaker :says ?object)>> .
|
|
891
|
-
?claim :source ?source .
|
|
892
|
-
}
|
|
893
|
-
```
|
|
894
|
-
|
|
895
|
-
This feature shows why Eyeleng has both parser tests and reasoning tests. It is not enough to accept the surface syntax. The parser must expand the syntax into triples that the engine can match.
|
|
896
|
-
|
|
897
|
-
## 28. BuiltInCall Coverage
|
|
898
|
-
|
|
899
|
-
The SRL grammar has a production named `BuiltInCall`. Eyeleng represents that production directly in `src/builtins.js` with `BUILTIN_SIGNATURES`.
|
|
900
|
-
|
|
901
|
-
The registry maps each grammar-level built-in name to its arity. That gives a clear path from syntax to execution:
|
|
902
|
-
|
|
903
|
-
```text
|
|
904
|
-
name in grammar -> registry entry -> parser accepts it -> evaluator checks arity -> implementation runs it
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
Built-ins sit at the boundary between syntax and reasoning. A rule body such as:
|
|
908
|
-
|
|
909
|
-
```srl
|
|
910
|
-
FILTER STRSTARTS(STR(?label), "A")
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
is not a triple pattern. It is a condition over the current solution mapping. The engine first binds `?label` by matching triples, then evaluates the built-in call. If the call returns true, the solution survives. If it returns false or raises an evaluation error, the solution is discarded.
|
|
914
|
-
|
|
915
|
-
Assignments use the same expression machinery:
|
|
916
|
-
|
|
917
|
-
```srl
|
|
918
|
-
SET(?slug := REPLACE(LCASE(STR(?name)), " ", "-"))
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
The assignment result becomes a new variable binding. Later body elements and the rule head can use `?slug`.
|
|
922
|
-
|
|
923
|
-
The parser treats unprefixed function names strictly. A bare function such as `LCASE(...)` is accepted because it is a grammar built-in. A custom function must be an IRI-named function call, such as `:startsWithA(...)` or `<http://example/fn>(...)`. This keeps Eyeleng close to the grammar instead of quietly accepting JavaScript-style helper names.
|
|
924
|
-
|
|
925
|
-
The most subtle built-in is `IF`. It is lazy. Only the selected branch is evaluated. That means this is safe when the condition is false:
|
|
926
|
-
|
|
927
|
-
```srl
|
|
928
|
-
SET(?value := IF(false, :missingFunction(), "safe"))
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
The missing function is part of the unchosen branch, so it is not evaluated.
|
|
932
|
-
|
|
933
|
-
`examples/builtin-call-complete.srl` exercises every built-in named by the grammar. `test/builtins.test.js` compares Eyeleng's registry against the complete BuiltInCall name list and runs the example as an executable smoke test.
|
|
934
|
-
|
|
935
|
-
## 29. RDF Rules Syntax Front-End
|
|
936
|
-
|
|
937
|
-
Eyeleng has two front-ends for rule sets: SRL text and RDF Rules syntax in Turtle. The RDF front-end does not replace the SRL parser. It translates an RDF graph that uses the `srl:` vocabulary into the same internal program shape that the SRL parser produces.
|
|
938
|
-
|
|
939
|
-
The important idea is this:
|
|
940
|
-
|
|
941
|
-
```text
|
|
942
|
-
Turtle/RDF input -> RDF graph -> srl:RuleSet translator -> internal program -> analyzer -> engine
|
|
943
|
-
```
|
|
944
|
-
|
|
945
|
-
That keeps the reasoning machine small. Once RDF syntax has been translated, the rest of Eyeleng does not care whether a rule originally came from this SRL text:
|
|
946
|
-
|
|
947
|
-
```srl
|
|
948
|
-
RULE { ?y :childOf ?x } WHERE { ?x :parentOf ?y }
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
or from this RDF description:
|
|
952
|
-
|
|
953
|
-
```turtle
|
|
954
|
-
[ a srl:Rule ;
|
|
955
|
-
srl:body (
|
|
956
|
-
[ srl:subject [ srl:varName "x" ] ;
|
|
957
|
-
srl:predicate :parentOf ;
|
|
958
|
-
srl:object [ srl:varName "y" ] ]
|
|
959
|
-
) ;
|
|
960
|
-
srl:head (
|
|
961
|
-
[ srl:subject [ srl:varName "y" ] ;
|
|
962
|
-
srl:predicate :childOf ;
|
|
963
|
-
srl:object [ srl:varName "x" ] ]
|
|
964
|
-
)
|
|
965
|
-
]
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
The RDF front-end lives in `src/rdfSyntax.js`. It has two layers.
|
|
969
|
-
|
|
970
|
-
First, `TurtleParser` reads a practical Turtle subset: prefixes, IRIs, prefixed names, literals, blank-node property lists, RDF collections, `a`, semicolon/comma abbreviation, and triple terms written as `<<(... )>>`. It expands collections into `rdf:first` and `rdf:rest` triples, just like the SRL parser does for collection syntax.
|
|
971
|
-
|
|
972
|
-
Second, `rdfDocumentToProgram` searches the parsed graph for `srl:RuleSet` nodes. For each rule set it reads:
|
|
973
|
-
|
|
974
|
-
- `srl:data`, whose value is an RDF list of data triples or triple terms,
|
|
975
|
-
- `srl:rules`, whose value is an RDF list of rule nodes,
|
|
976
|
-
- each rule's `srl:body` and `srl:head` lists,
|
|
977
|
-
- `srl:filter`, `srl:assign`, and `srl:not` body elements,
|
|
978
|
-
- variable nodes written with `srl:varName`, and expression variables written with `shnex:var`.
|
|
979
|
-
|
|
980
|
-
RDF expression nodes such as:
|
|
981
|
-
|
|
982
|
-
```turtle
|
|
983
|
-
[ sparql:less-than ( [ shnex:var "age" ] 18 ) ]
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
become the same expression AST used by SRL `FILTER(?age < 18)`. Supported operator nodes include comparison, arithmetic, boolean connectives, and calls that map onto Eyeleng's BuiltInCall registry.
|
|
987
|
-
|
|
988
|
-
The CLI auto-detects `.ttl` input as RDF syntax, and also accepts explicit syntax selection:
|
|
989
|
-
|
|
990
|
-
```sh
|
|
991
|
-
./eyeleng.js --syntax rdf examples/basic-ruleset.ttl
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
If a Turtle file contains several rule sets, select one with `--ruleset`:
|
|
995
|
-
|
|
996
|
-
```sh
|
|
997
|
-
./eyeleng.js --syntax rdf --ruleset :familyRules examples/basic-ruleset.ttl
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
The RDF syntax examples in `examples/*.ttl` are executable regression files. One of them, `w3c-rule-set-snippet.ttl`, is adapted from the W3C `data-shapes` repository's `shacl12-rules/rules-rdf-syntax/test-rules.ttl` file and exercises the same core vocabulary shape: `srl:RuleSet`, RDF lists, rule nodes, triple objects, filters, assignments, and triple terms in data blocks.
|
|
1001
|
-
|
|
1002
|
-
This front-end is intentionally not a full SHACL validator. It can execute RDF Rules syntax rule sets, but it does not validate arbitrary RDF files against the `srl-sh:` SHACL shapes from the repository.
|
|
1003
|
-
|
|
1004
|
-
## 30. Known Limitations
|
|
1005
|
-
|
|
1006
|
-
Eyeleng is not a standards conformance claim. The most important remaining limitations are:
|
|
1007
|
-
|
|
1008
|
-
- it does not implement SHACL validation or validation reports,
|
|
1009
|
-
- it does not implement a complete RDF 1.2 parser,
|
|
1010
|
-
- it does not implement every SPARQL expression edge case,
|
|
1011
|
-
- it keeps property paths deliberately restricted,
|
|
1012
|
-
- RDF Rules syntax support is a practical front-end rather than a full SHACL shapes validation layer,
|
|
1013
|
-
- performance-critical paths are indexed for common rule workloads, but this remains a compact implementation, not a production RDF database.
|
|
1014
|
-
|
|
1015
|
-
The best way to read these limitations is as architectural boundaries. The rule engine derives triples. A SHACL validator checks shapes and emits validation reports. Those can be connected later, but they are different machines.
|
|
1016
|
-
|
|
1017
|
-
## 31. How to Extend Eyeleng Safely
|
|
1018
|
-
|
|
1019
|
-
When adding a feature, preserve the pipeline:
|
|
1020
|
-
|
|
1021
|
-
```text
|
|
1022
|
-
syntax -> AST/program -> analysis -> evaluation -> formatting
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
Avoid making the evaluator parse strings. Parsing belongs in `parser.js` or `rdfSyntax.js`. Avoid making the parser derive triples. Inference belongs in `engine.js`.
|
|
1026
|
-
|
|
1027
|
-
A safe extension usually needs:
|
|
1028
|
-
|
|
1029
|
-
1. syntax support,
|
|
1030
|
-
2. one focused example,
|
|
1031
|
-
3. a parser/API test,
|
|
1032
|
-
4. an execution test,
|
|
1033
|
-
5. a handbook update,
|
|
1034
|
-
6. bundle regeneration.
|
|
1035
|
-
|
|
1036
|
-
Useful extension directions include:
|
|
1037
|
-
|
|
1038
|
-
- more RDF Rules syntax vocabulary coverage,
|
|
1039
|
-
- more RDF 1.2 parser coverage,
|
|
1040
|
-
- more SPARQL-compatible expression behavior,
|
|
1041
|
-
- better diagnostics for non-well-formed rule sets,
|
|
1042
|
-
- more specialized indexes and query planning for larger graphs.
|
|
1043
|
-
|
|
1044
|
-
## 32. The Big Picture
|
|
1045
|
-
|
|
1046
|
-
Eyeleng is a Datalog-like forward reasoner over RDF-style triples.
|
|
1047
|
-
|
|
1048
|
-
The heart of the system is:
|
|
1049
|
-
|
|
1050
|
-
```text
|
|
1051
|
-
match rule bodies
|
|
1052
|
-
instantiate rule heads
|
|
1053
|
-
add new triples
|
|
1054
|
-
repeat by dependency layer until stable
|
|
1055
|
-
```
|
|
1056
|
-
|
|
1057
|
-
Everything else supports that loop:
|
|
1058
|
-
|
|
1059
|
-
- the tokenizer and parser create rules and data,
|
|
1060
|
-
- the RDF syntax front-end translates Turtle rule sets,
|
|
1061
|
-
- terms define equality,
|
|
1062
|
-
- the store makes matching possible,
|
|
1063
|
-
- builtins make conditions and assignments useful,
|
|
1064
|
-
- analysis makes negation deterministic,
|
|
1065
|
-
- imports assemble rule sets,
|
|
1066
|
-
- formatting explains results,
|
|
1067
|
-
- tests preserve behavior,
|
|
1068
|
-
- the bundle makes the tool easy to run.
|
|
1069
|
-
|
|
1070
|
-
Understanding Eyeleng means understanding how a declarative rule set becomes a deterministic computation over a growing set of facts.
|