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/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.