eyeling 1.10.5 → 1.10.6
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 +1693 -0
- package/README.md +26 -169
- package/package.json +2 -1
package/HANDBOOK.md
ADDED
|
@@ -0,0 +1,1693 @@
|
|
|
1
|
+
# Inside Eyeling
|
|
2
|
+
|
|
3
|
+
## A compact Notation3 reasoner in JavaScript — a handbook
|
|
4
|
+
|
|
5
|
+
> This handbook is written for a computer science student who wants to understand Eyeling as *code* and as a *reasoning machine*.
|
|
6
|
+
> It’s meant to be read linearly, but each chapter stands on its own.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Preface](#preface)
|
|
12
|
+
- [Chapter 1 — The execution model in one picture](#ch01)
|
|
13
|
+
- [Chapter 2 — The repository, as a guided reading path](#ch02)
|
|
14
|
+
- [Chapter 3 — The data model: terms, triples, formulas, rules](#ch03)
|
|
15
|
+
- [Chapter 4 — From characters to AST: lexing and parsing](#ch04)
|
|
16
|
+
- [Chapter 5 — Rule normalization: “compile-time” semantics](#ch05)
|
|
17
|
+
- [Chapter 6 — Equality, alpha-equivalence, and unification](#ch06)
|
|
18
|
+
- [Chapter 7 — Facts as a database: indexing and fast duplicate checks](#ch07)
|
|
19
|
+
- [Chapter 8 — Backward chaining: the proof engine](#ch08)
|
|
20
|
+
- [Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules](#ch09)
|
|
21
|
+
- [Chapter 10 — Scoped closure, priorities, and `log:conclusion`](#ch10)
|
|
22
|
+
- [Chapter 11 — Built-ins as a standard library](#ch11)
|
|
23
|
+
- [Chapter 12 — Dereferencing and web-like semantics](#ch12)
|
|
24
|
+
- [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
|
|
25
|
+
- [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
|
|
26
|
+
- [Chapter 15 — A worked example: Socrates, step by step](#ch15)
|
|
27
|
+
- [Chapter 16 — Extending Eyeling (without breaking it)](#ch16)
|
|
28
|
+
- [Epilogue](#epilogue)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<a id="preface"></a>
|
|
33
|
+
## Preface: what Eyeling is (and what it is not)
|
|
34
|
+
|
|
35
|
+
Eyeling is a small Notation3 (N3) reasoner implemented in JavaScript. Its job is to take:
|
|
36
|
+
|
|
37
|
+
1. **Facts** (RDF-like triples), and
|
|
38
|
+
2. **Rules** written in N3’s implication style (`=>` and `<=`),
|
|
39
|
+
|
|
40
|
+
and compute consequences until nothing new follows.
|
|
41
|
+
|
|
42
|
+
If you’ve seen Datalog or Prolog, the shape will feel familiar. Eyeling blends both:
|
|
43
|
+
|
|
44
|
+
- **Forward chaining** (like Datalog saturation) for `=>` rules.
|
|
45
|
+
- **Backward chaining** (like Prolog goal solving) for `<=` rules *and* for built-in predicates.
|
|
46
|
+
|
|
47
|
+
That last point is the heart of Eyeling’s design: *forward rules are executed by proving their bodies using a backward engine*. This lets forward rules depend on computations and “virtual predicates” without explicitly materializing everything as facts.
|
|
48
|
+
|
|
49
|
+
Eyeling deliberately keeps the implementation small and dependency-free:
|
|
50
|
+
- the published package includes a single bundled file (`eyeling.js`)
|
|
51
|
+
- the source is organized into `lib/*` modules that read like a miniature compiler + logic engine.
|
|
52
|
+
|
|
53
|
+
This handbook is a tour of that miniature system.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
<a id="ch01"></a>
|
|
58
|
+
## Chapter 1 — The execution model in one picture
|
|
59
|
+
|
|
60
|
+
Let’s name the pieces:
|
|
61
|
+
|
|
62
|
+
- A **fact** is a triple `(subject, predicate, object)`.
|
|
63
|
+
- A **forward rule** has the form `{ body } => { head }.`
|
|
64
|
+
Read: if the body is provable, assert the head.
|
|
65
|
+
- A **backward rule** has the form `{ head } <= { body }.`
|
|
66
|
+
Read: to prove the head, prove the body.
|
|
67
|
+
|
|
68
|
+
Eyeling runs like this:
|
|
69
|
+
|
|
70
|
+
1. Parse the document into:
|
|
71
|
+
- an initial fact set `F`
|
|
72
|
+
- forward rules `R_f`
|
|
73
|
+
- backward rules `R_b`
|
|
74
|
+
2. Repeat until fixpoint:
|
|
75
|
+
- for each forward rule `r ∈ R_f`:
|
|
76
|
+
- use the backward prover to find substitutions that satisfy `r.body` using:
|
|
77
|
+
- the current facts
|
|
78
|
+
- backward rules
|
|
79
|
+
- built-ins
|
|
80
|
+
- for each solution, instantiate and add `r.head`
|
|
81
|
+
|
|
82
|
+
A good mental model is:
|
|
83
|
+
|
|
84
|
+
> **Forward chaining is “outer control”. Backward chaining is the “query engine” used inside each rule firing.**
|
|
85
|
+
|
|
86
|
+
A sketch:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
FORWARD LOOP (saturation)
|
|
91
|
+
for each forward rule r:
|
|
92
|
+
solutions = PROVE(r.body) <-- backward reasoning + builtins
|
|
93
|
+
for each s in solutions:
|
|
94
|
+
emit instantiate(r.head, s)
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Because `PROVE` can call built-ins (math, string, list, crypto, dereferencing…), forward rules can compute fresh bindings as part of their condition.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
<a id="ch02"></a>
|
|
103
|
+
## Chapter 2 — The repository, as a guided reading path
|
|
104
|
+
|
|
105
|
+
If you want to follow the code in the same order Eyeling “thinks”, read:
|
|
106
|
+
|
|
107
|
+
1. `lib/prelude.js` — the AST (terms, triples, rules), namespaces, prefix handling.
|
|
108
|
+
2. `lib/lexer.js` — N3/Turtle-ish tokenization.
|
|
109
|
+
3. `lib/parser.js` — parsing tokens into triples, formulas, and rules.
|
|
110
|
+
4. `lib/rules.js` — small rule “compiler passes” (blank lifting, constraint delaying).
|
|
111
|
+
5. `lib/engine.js` — the core engine:
|
|
112
|
+
- equality + alpha equivalence for formulas
|
|
113
|
+
- unification + substitutions
|
|
114
|
+
- indexing facts and backward rules
|
|
115
|
+
- backward goal proving (`proveGoals`)
|
|
116
|
+
- forward saturation (`forwardChain`)
|
|
117
|
+
- built-ins (`evalBuiltin`)
|
|
118
|
+
- scoped-closure machinery (for `log:*In` and includes tests)
|
|
119
|
+
- explanations and output construction
|
|
120
|
+
6. `lib/deref.js` — synchronous dereferencing for `log:content` / `log:semantics`.
|
|
121
|
+
7. `lib/printing.js` — conversion back to N3 text.
|
|
122
|
+
8. `lib/cli.js` + `lib/entry.js` — command-line wiring and bundle entry exports.
|
|
123
|
+
9. `index.js` — the npm API wrapper (spawns the bundled CLI synchronously).
|
|
124
|
+
|
|
125
|
+
This is almost literally a tiny compiler pipeline:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
text → tokens → AST (facts + rules) → engine → derived facts → printer
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
<a id="ch03"></a>
|
|
136
|
+
## Chapter 3 — The data model: terms, triples, formulas, rules (`lib/prelude.js`)
|
|
137
|
+
|
|
138
|
+
Eyeling uses a small AST. You can think of it as the “instruction set” for the rest of the reasoner.
|
|
139
|
+
|
|
140
|
+
### 3.1 Terms
|
|
141
|
+
|
|
142
|
+
A **Term** is one of:
|
|
143
|
+
|
|
144
|
+
- `Iri(value)` — an absolute IRI string
|
|
145
|
+
- `Literal(value)` — stored as raw lexical form (e.g. `"hi"@en`, `12`, `"2020-01-01"^^<dt>`)
|
|
146
|
+
- `Var(name)` — variable name without the leading `?`
|
|
147
|
+
- `Blank(label)` — blank node label like `_:b1`
|
|
148
|
+
- `ListTerm(elems)` — a concrete N3 list `(a b c)`
|
|
149
|
+
- `OpenListTerm(prefix, tailVar)` — a “list with unknown tail”, used for list unification patterns
|
|
150
|
+
- `GraphTerm(triples)` — a quoted formula `{ ... }` as a first-class term
|
|
151
|
+
|
|
152
|
+
That last one is special: N3 allows formulas as terms, so Eyeling must treat graphs as matchable data.
|
|
153
|
+
|
|
154
|
+
### 3.2 Triples and rules
|
|
155
|
+
|
|
156
|
+
A triple is:
|
|
157
|
+
|
|
158
|
+
- `Triple(s, p, o)` where each position is a Term.
|
|
159
|
+
|
|
160
|
+
A rule is:
|
|
161
|
+
|
|
162
|
+
- `Rule(premiseTriples, conclusionTriples, isForward, isFuse, headBlankLabels)`
|
|
163
|
+
|
|
164
|
+
Two details matter later:
|
|
165
|
+
|
|
166
|
+
1. **Inference fuse**: a forward rule whose conclusion is the literal `false` acts as a hard failure. (More in Chapter 10.)
|
|
167
|
+
2. **`headBlankLabels`** records which blank node labels occur *explicitly in the head* of a rule. Those blanks are treated as existentials and get skolemized per firing. (Chapter 9.)
|
|
168
|
+
|
|
169
|
+
### 3.3 Interning
|
|
170
|
+
|
|
171
|
+
Eyeling interns IRIs and Literals by string value. Interning is a quiet performance trick with big consequences:
|
|
172
|
+
|
|
173
|
+
- repeated IRIs become pointer-equal
|
|
174
|
+
- indexing is cheaper
|
|
175
|
+
- comparisons are faster and allocations drop.
|
|
176
|
+
|
|
177
|
+
Terms are treated as immutable: once interned, the code assumes you won’t mutate `.value`.
|
|
178
|
+
|
|
179
|
+
### 3.4 Prefix environment
|
|
180
|
+
|
|
181
|
+
`PrefixEnv` holds prefix mappings and a base IRI. It provides:
|
|
182
|
+
|
|
183
|
+
- expansion (`ex:foo` → full IRI)
|
|
184
|
+
- shrinking for printing (full IRI → `ex:foo` when possible)
|
|
185
|
+
- default prefixes for RDF/RDFS/XSD/log/math/string/list/time/genid.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
<a id="ch04"></a>
|
|
190
|
+
## Chapter 4 — From characters to AST: lexing and parsing (`lib/lexer.js`, `lib/parser.js`)
|
|
191
|
+
|
|
192
|
+
Eyeling’s parser is intentionally pragmatic: it aims to accept “the stuff people actually write” in N3/Turtle, including common shorthand.
|
|
193
|
+
|
|
194
|
+
### 4.1 Lexing: tokens, not magic
|
|
195
|
+
|
|
196
|
+
The lexer turns the input into tokens like:
|
|
197
|
+
|
|
198
|
+
- punctuation: `{ } ( ) [ ] , ; .`
|
|
199
|
+
- operators: `=>`, `<=`, `=`, `!`, `^`
|
|
200
|
+
- directives: `@prefix`, `@base`, and also SPARQL-style `PREFIX`, `BASE`
|
|
201
|
+
- variables `?x`
|
|
202
|
+
- blanks `_:b1`
|
|
203
|
+
- IRIREF `<...>`
|
|
204
|
+
- qnames `rdf:type`, `:local`
|
|
205
|
+
- literals: strings (short and long), numbers, `true`/`false`, `^^` datatypes, `@en` language tags
|
|
206
|
+
- `#` comments
|
|
207
|
+
|
|
208
|
+
Parsing becomes dramatically simpler because tokenization already decided where strings end, where numbers are, and so on.
|
|
209
|
+
|
|
210
|
+
### 4.2 Parsing triples, with Turtle-style convenience
|
|
211
|
+
|
|
212
|
+
The parser supports:
|
|
213
|
+
|
|
214
|
+
- predicate/object lists with `;` and `,`
|
|
215
|
+
- blank node property lists `[ :p :o; :q :r ]`
|
|
216
|
+
- collections `( ... )` as `ListTerm`
|
|
217
|
+
- quoted formulas `{ ... }` as `GraphTerm`
|
|
218
|
+
- variables, blanks, literals, qnames, IRIREFs
|
|
219
|
+
- keyword-ish sugar like `is ... of` and inverse arrows
|
|
220
|
+
- path operators `!` and `^` that may generate helper triples via fresh blanks
|
|
221
|
+
|
|
222
|
+
A nice detail: the parser maintains a `pendingTriples` list used when certain syntactic forms expand into helper triples (for example, some path/property-list expansions). It ensures the “surface statement” still emits all required triples even if the subject itself was syntactic sugar.
|
|
223
|
+
|
|
224
|
+
### 4.3 Parsing rules: `=>`, `<=`, and log idioms
|
|
225
|
+
|
|
226
|
+
At the top level, the parser recognizes:
|
|
227
|
+
|
|
228
|
+
- `{ P } => { C } .` as a forward rule
|
|
229
|
+
- `{ H } <= { B } .` as a backward rule
|
|
230
|
+
|
|
231
|
+
It also normalizes top-level triples of the form:
|
|
232
|
+
|
|
233
|
+
- `{ P } log:implies { C } .`
|
|
234
|
+
- `{ H } log:impliedBy { B } .`
|
|
235
|
+
|
|
236
|
+
into the same internal Rule objects. That means you can write rules either as operators (`=>`, `<=`) or as explicit `log:` predicates.
|
|
237
|
+
|
|
238
|
+
### 4.4 `true` and `false` as rule endpoints
|
|
239
|
+
|
|
240
|
+
Eyeling treats two literals specially in rule positions:
|
|
241
|
+
|
|
242
|
+
- `true` stands for the empty formula `{}` (an empty premise or head).
|
|
243
|
+
- `false` is used for inference fuses (`{ ... } => false.`).
|
|
244
|
+
|
|
245
|
+
So these are valid patterns:
|
|
246
|
+
|
|
247
|
+
```n3
|
|
248
|
+
true => { :Program :loaded true }.
|
|
249
|
+
{ ?x :p :q } => false.
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Internally:
|
|
253
|
+
|
|
254
|
+
* `true` becomes “empty triple list”
|
|
255
|
+
* `false` becomes “no head triples” *plus* the `isFuse` flag if forward.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
<a id="ch05"></a>
|
|
260
|
+
## Chapter 5 — Rule normalization: “compile-time” semantics (`lib/rules.js`)
|
|
261
|
+
|
|
262
|
+
Before rules hit the engine, Eyeling performs two lightweight transformations.
|
|
263
|
+
|
|
264
|
+
### 5.1 Lifting blank nodes in rule bodies into variables
|
|
265
|
+
|
|
266
|
+
In N3 practice, blanks in *rule premises* behave like universally-quantified placeholders. Eyeling implements this by converting `Blank(label)` to `Var(_bN)` in the premise only.
|
|
267
|
+
|
|
268
|
+
So a premise like:
|
|
269
|
+
|
|
270
|
+
```n3
|
|
271
|
+
{ _:x :p ?y. } => { ... }.
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
acts like:
|
|
275
|
+
|
|
276
|
+
```n3
|
|
277
|
+
{ ?_b1 :p ?y. } => { ... }.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This avoids the “existential in the body” trap and matches how most rule authors expect N3 to behave.
|
|
281
|
+
|
|
282
|
+
Blanks in the **conclusion** are *not* lifted — they remain blanks and later become existentials (Chapter 9).
|
|
283
|
+
|
|
284
|
+
### 5.2 Delaying constraints
|
|
285
|
+
|
|
286
|
+
Some built-ins don’t generate bindings; they only test conditions:
|
|
287
|
+
|
|
288
|
+
* `math:greaterThan`, `math:lessThan`, `math:equalTo`, …
|
|
289
|
+
* `string:matches`, `string:contains`, …
|
|
290
|
+
* `log:notIncludes`, `log:forAllIn`, `log:outputString`, …
|
|
291
|
+
|
|
292
|
+
Eyeling treats these as “constraints” and moves them to the *end* of a forward rule premise. This is a Prolog-style heuristic:
|
|
293
|
+
|
|
294
|
+
> Bind variables first; only then run pure checks.
|
|
295
|
+
|
|
296
|
+
It’s not logically necessary, but it improves the chance that constraints run with variables already grounded, reducing wasted search.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
<a id="ch06"></a>
|
|
301
|
+
## Chapter 6 — Equality, alpha-equivalence, and unification (`lib/engine.js`)
|
|
302
|
+
|
|
303
|
+
Once you enter `engine.js`, you enter the “physics layer.” Everything else depends on the correctness of:
|
|
304
|
+
|
|
305
|
+
* equality and normalization (especially for literals)
|
|
306
|
+
* alpha-equivalence for formulas
|
|
307
|
+
* unification and substitution application
|
|
308
|
+
|
|
309
|
+
### 6.1 Two equalities: structural vs alpha-equivalent
|
|
310
|
+
|
|
311
|
+
Eyeling has ordinary structural equality (term-by-term) for most terms.
|
|
312
|
+
|
|
313
|
+
But **quoted formulas** (`GraphTerm`) demand something stronger. Two formulas should match even if their internal blank/variable names differ, as long as the structure is the same.
|
|
314
|
+
|
|
315
|
+
That’s alpha-equivalence:
|
|
316
|
+
|
|
317
|
+
* `{ _:x :p ?y. }` should match `{ _:z :p ?w. }`
|
|
318
|
+
|
|
319
|
+
Eyeling implements alpha-equivalence by checking whether there exists a consistent renaming mapping between the two formulas’ variables/blanks that makes the triples match.
|
|
320
|
+
|
|
321
|
+
### 6.2 Groundness: “variables inside formulas don’t leak”
|
|
322
|
+
|
|
323
|
+
Eyeling makes a deliberate choice about *groundness*:
|
|
324
|
+
|
|
325
|
+
* a triple is “ground” if it has no free variables in normal positions
|
|
326
|
+
* **variables inside a `GraphTerm` do not make the surrounding triple non-ground**
|
|
327
|
+
|
|
328
|
+
This is encoded in functions like `isGroundTermInGraph`. It’s what makes it possible to assert and store triples that *mention formulas with variables* as data.
|
|
329
|
+
|
|
330
|
+
### 6.3 Substitutions: chaining and application
|
|
331
|
+
|
|
332
|
+
A substitution is a plain JS object:
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
{ X: Term, Y: Term, ... }
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
When applying substitutions, Eyeling follows chains:
|
|
339
|
+
|
|
340
|
+
* if `X → Var(Y)` and `Y → Iri(...)`, applying to `X` yields the IRI.
|
|
341
|
+
|
|
342
|
+
This matters because unification can bind variables to variables; it’s normal in logic programming, and you want `applySubst` to “chase the link” until it reaches a stable term.
|
|
343
|
+
|
|
344
|
+
### 6.4 Unification: the core operation
|
|
345
|
+
|
|
346
|
+
Unification is implemented in `unifyTerm` / `unifyTriple`, with support for:
|
|
347
|
+
|
|
348
|
+
* variable binding with occurs check
|
|
349
|
+
* list unification (elementwise)
|
|
350
|
+
* open-list unification (prefix + tail variable)
|
|
351
|
+
* formula unification via graph unification:
|
|
352
|
+
|
|
353
|
+
* fast path: identical triple list
|
|
354
|
+
* otherwise: backtracking order-insensitive matching while threading the substitution
|
|
355
|
+
|
|
356
|
+
There are two key traits of Eyeling’s graph unification:
|
|
357
|
+
|
|
358
|
+
1. It’s *set-like*: order doesn’t matter.
|
|
359
|
+
2. It’s *substitution-threaded*: choices made while matching one triple restrict the remaining matches, just like Prolog.
|
|
360
|
+
|
|
361
|
+
### 6.5 Literals: lexical vs semantic equality
|
|
362
|
+
|
|
363
|
+
Eyeling keeps literal values as raw strings, but it parses and normalizes where needed:
|
|
364
|
+
|
|
365
|
+
* `literalParts(lit)` splits lexical form and datatype IRI
|
|
366
|
+
* it recognizes RDF JSON datatype (`rdf:JSON` / `<...rdf#JSON>`)
|
|
367
|
+
* it includes caches for numeric parsing, integer parsing (`BigInt`), and numeric metadata.
|
|
368
|
+
|
|
369
|
+
This lets built-ins and fast-key indexing treat some different lexical spellings as the same value (for example, normalizing `"abc"` and `"abc"^^xsd:string` in the fast-key path).
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
<a id="ch07"></a>
|
|
374
|
+
## Chapter 7 — Facts as a database: indexing and fast duplicate checks
|
|
375
|
+
|
|
376
|
+
Reasoning is mostly “join-like” operations: match a goal triple against known facts. Doing this naively is too slow, so Eyeling builds indexes on top of a plain array.
|
|
377
|
+
|
|
378
|
+
### 7.1 The fact store
|
|
379
|
+
|
|
380
|
+
Facts live in an array `facts: Triple[]`.
|
|
381
|
+
|
|
382
|
+
Eyeling attaches hidden (non-enumerable) index fields:
|
|
383
|
+
|
|
384
|
+
* `facts.__byPred: Map<predicateIRI, Triple[]>`
|
|
385
|
+
* `facts.__byPS: Map<predicateIRI, Map<subjectKey, Triple[]>>`
|
|
386
|
+
* `facts.__byPO: Map<predicateIRI, Map<objectKey, Triple[]>>`
|
|
387
|
+
* `facts.__keySet: Set<string>` for a fast-path “S\tP\tO” key when all terms are IRI/Literal-like
|
|
388
|
+
|
|
389
|
+
The “fast key” only exists when `termFastKey` succeeds for all three terms.
|
|
390
|
+
|
|
391
|
+
### 7.2 Candidate selection: pick the smallest bucket
|
|
392
|
+
|
|
393
|
+
When proving a goal with IRI predicate, Eyeling computes candidate facts by:
|
|
394
|
+
|
|
395
|
+
1. restricting to predicate bucket
|
|
396
|
+
2. optionally narrowing further by subject or object fast key
|
|
397
|
+
3. choosing the smaller of (p,s) vs (p,o) when both exist
|
|
398
|
+
|
|
399
|
+
This is a cheap selectivity heuristic. In type-heavy RDF, `(p,o)` is often extremely selective (e.g., `rdf:type` + a class IRI), so the PO index can be a major speed win.
|
|
400
|
+
|
|
401
|
+
### 7.3 Duplicate detection is careful about blanks
|
|
402
|
+
|
|
403
|
+
A tempting optimization would be “treat two triples as duplicates modulo blank renaming.” Eyeling does **not** do this globally, because it would be unsound: different blank labels represent different existentials unless explicitly linked.
|
|
404
|
+
|
|
405
|
+
So:
|
|
406
|
+
|
|
407
|
+
* fast-key dedup works for IRI/Literal-only triples
|
|
408
|
+
* otherwise, it falls back to real triple equality on actual blank labels.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
<a id="ch08"></a>
|
|
413
|
+
## Chapter 8 — Backward chaining: the proof engine (`proveGoals`)
|
|
414
|
+
|
|
415
|
+
Eyeling’s backward prover is an iterative depth-first search (DFS) that looks a lot like Prolog’s SLD resolution, but written explicitly with a stack to avoid JS recursion limits.
|
|
416
|
+
|
|
417
|
+
### 8.1 Proof states
|
|
418
|
+
|
|
419
|
+
A proof state contains:
|
|
420
|
+
|
|
421
|
+
* `goals`: remaining goal triples
|
|
422
|
+
* `subst`: current substitution
|
|
423
|
+
* `depth`: current depth (used for compaction heuristics)
|
|
424
|
+
* `visited`: previously-seen goals (loop prevention)
|
|
425
|
+
|
|
426
|
+
### 8.2 The proving loop
|
|
427
|
+
|
|
428
|
+
At each step:
|
|
429
|
+
|
|
430
|
+
1. If no goals remain: emit the current substitution as a solution.
|
|
431
|
+
2. Otherwise:
|
|
432
|
+
|
|
433
|
+
* take the first goal
|
|
434
|
+
* apply the current substitution to it
|
|
435
|
+
* attempt to satisfy it in three ways:
|
|
436
|
+
|
|
437
|
+
1. built-ins
|
|
438
|
+
2. facts
|
|
439
|
+
3. backward rules
|
|
440
|
+
|
|
441
|
+
Eyeling’s order is intentional: built-ins often bind variables cheaply; rules expand search trees.
|
|
442
|
+
|
|
443
|
+
### 8.3 Built-ins: return *deltas*, not full substitutions
|
|
444
|
+
|
|
445
|
+
A built-in is evaluated as:
|
|
446
|
+
|
|
447
|
+
```js
|
|
448
|
+
deltas = evalBuiltin(goal0, {}, facts, backRules, ...)
|
|
449
|
+
for delta in deltas:
|
|
450
|
+
composed = composeSubst(currentSubst, delta)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
So built-ins behave like relations that can generate zero, one, or many possible bindings.
|
|
454
|
+
|
|
455
|
+
This is important: a list generator might yield many deltas; a numeric test yields zero or one.
|
|
456
|
+
|
|
457
|
+
### 8.4 Loop prevention: a simple visited list
|
|
458
|
+
|
|
459
|
+
Eyeling prevents obvious infinite recursion by skipping a goal if it is already in the `visited` list. This is a pragmatic check; it doesn’t implement full tabling, but it avoids the most common “A depends on A” loops.
|
|
460
|
+
|
|
461
|
+
### 8.5 Backward rules: indexed by head predicate
|
|
462
|
+
|
|
463
|
+
Backward rules are indexed in `backRules.__byHeadPred`. When proving a goal with IRI predicate `p`, Eyeling retrieves:
|
|
464
|
+
|
|
465
|
+
* `rules whose head predicate is p`
|
|
466
|
+
* plus `__wildHeadPred` for rules whose head predicate is not an IRI (rare, but supported)
|
|
467
|
+
|
|
468
|
+
For each candidate rule:
|
|
469
|
+
|
|
470
|
+
1. standardize it apart (fresh variables)
|
|
471
|
+
2. unify the rule head with the goal
|
|
472
|
+
3. append the rule body goals in front of the remaining goals
|
|
473
|
+
|
|
474
|
+
That “standardize apart” step is essential. Without it, reusing a rule multiple times would accidentally share variables across invocations, producing incorrect bindings.
|
|
475
|
+
|
|
476
|
+
### 8.6 Substitution compaction: keeping DFS from going quadratic
|
|
477
|
+
|
|
478
|
+
Deep backward chains can create large substitutions. If you copy a growing object at every step, you can accidentally get O(depth²) behavior.
|
|
479
|
+
|
|
480
|
+
Eyeling avoids that with `maybeCompactSubst`:
|
|
481
|
+
|
|
482
|
+
* if depth is high or substitution is large, it keeps only bindings relevant to:
|
|
483
|
+
|
|
484
|
+
* the remaining goals
|
|
485
|
+
* variables from the original goal list (“answer variables”)
|
|
486
|
+
* plus variables transitively referenced inside kept bindings
|
|
487
|
+
|
|
488
|
+
This is semantics-preserving for the ongoing proof search, but dramatically improves performance on deep recursive proofs.
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
<a id="ch09"></a>
|
|
493
|
+
## Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules (`forwardChain`)
|
|
494
|
+
|
|
495
|
+
Forward chaining is Eyeling’s outer control loop. It is where facts get added and the closure grows.
|
|
496
|
+
|
|
497
|
+
### 9.1 The shape of saturation
|
|
498
|
+
|
|
499
|
+
Eyeling loops until no new facts are added. Inside that loop, it scans every forward rule and tries to fire it.
|
|
500
|
+
|
|
501
|
+
A simplified view:
|
|
502
|
+
|
|
503
|
+
```text
|
|
504
|
+
repeat
|
|
505
|
+
changed = false
|
|
506
|
+
for each forward rule r:
|
|
507
|
+
sols = proveGoals(r.premise, facts, backRules)
|
|
508
|
+
for each solution s:
|
|
509
|
+
for each head triple h in r.conclusion:
|
|
510
|
+
inst = applySubst(h, s)
|
|
511
|
+
inst = skolemizeHeadBlanks(inst)
|
|
512
|
+
if inst is ground and new:
|
|
513
|
+
add inst to facts
|
|
514
|
+
changed = true
|
|
515
|
+
until not changed
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### 9.2 Strict-ground head optimization
|
|
519
|
+
|
|
520
|
+
There is a nice micro-compiler optimization in `runFixpoint()`:
|
|
521
|
+
|
|
522
|
+
If a rule’s head is *strictly ground* (no vars, no blanks, no open lists, even inside formulas), and it contains no head blanks, then the head does not depend on *which* body solution you choose.
|
|
523
|
+
|
|
524
|
+
In that case:
|
|
525
|
+
|
|
526
|
+
* Eyeling only needs **one** proof of the body.
|
|
527
|
+
* And if all head triples are already known, it can skip proving the body entirely.
|
|
528
|
+
|
|
529
|
+
This is a surprisingly effective optimization for “axiom-like” rules with constant heads.
|
|
530
|
+
|
|
531
|
+
### 9.3 Existentials: skolemizing head blanks
|
|
532
|
+
|
|
533
|
+
Blank nodes in the **rule head** represent existentials: “there exists something such that…”
|
|
534
|
+
|
|
535
|
+
Eyeling handles this by replacing head blank labels with fresh blank labels of the form:
|
|
536
|
+
|
|
537
|
+
* `_:sk_0`, `_:sk_1`, …
|
|
538
|
+
|
|
539
|
+
But it does something subtle and important: it caches skolemization per (rule firing, head blank label), so that the *same* firing instance doesn’t keep generating new blanks across outer iterations.
|
|
540
|
+
|
|
541
|
+
The “firing instance” is keyed by a deterministic string derived from the instantiated body (“firingKey”). This stabilizes the closure and prevents “existential churn.”
|
|
542
|
+
|
|
543
|
+
### 9.4 Inference fuses: `{ ... } => false`
|
|
544
|
+
|
|
545
|
+
A rule whose conclusion is `false` is treated as a hard failure. During forward chaining:
|
|
546
|
+
|
|
547
|
+
* Eyeling proves the premise (it only needs one solution)
|
|
548
|
+
* if the premise is provable, it prints a message and exits with status code 2
|
|
549
|
+
|
|
550
|
+
This is Eyeling’s way to express constraints and detect inconsistencies.
|
|
551
|
+
|
|
552
|
+
### 9.5 Rule-producing rules (meta-rules)
|
|
553
|
+
|
|
554
|
+
Eyeling treats certain derived triples as *new rules*:
|
|
555
|
+
|
|
556
|
+
* `log:implies` and `log:impliedBy` where subject/object are formulas
|
|
557
|
+
* it also accepts the literal `true` as an empty formula `{}` on either side
|
|
558
|
+
|
|
559
|
+
So these are “rule triples”:
|
|
560
|
+
|
|
561
|
+
```n3
|
|
562
|
+
{ ... } log:implies { ... }.
|
|
563
|
+
true log:implies { ... }.
|
|
564
|
+
{ ... } log:impliedBy true.
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
When such a triple is derived in a forward rule head:
|
|
568
|
+
|
|
569
|
+
1. Eyeling adds it as a fact (so you can inspect it), and
|
|
570
|
+
2. it *promotes* it into a live rule by constructing a new `Rule` object and inserting it into the forward or backward rule list.
|
|
571
|
+
|
|
572
|
+
This is meta-programming: your rules can generate new rules during reasoning.
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
<a id="ch10"></a>
|
|
577
|
+
## Chapter 10 — Scoped closure, priorities, and `log:conclusion`
|
|
578
|
+
|
|
579
|
+
Some `log:` built-ins talk about “what is included in the closure” or “collect all solutions.” These are tricky in a forward-chaining engine because the closure is *evolving*.
|
|
580
|
+
|
|
581
|
+
Eyeling addresses this with a disciplined two-phase strategy and an optional priority mechanism.
|
|
582
|
+
|
|
583
|
+
### 10.1 The two-phase outer loop (Phase A / Phase B)
|
|
584
|
+
|
|
585
|
+
Forward chaining runs inside an *outer loop* that alternates:
|
|
586
|
+
|
|
587
|
+
* **Phase A**: scoped built-ins are disabled (they “delay” by failing)
|
|
588
|
+
|
|
589
|
+
* Eyeling saturates normally to a fixpoint
|
|
590
|
+
|
|
591
|
+
* then Eyeling freezes a snapshot of the saturated facts
|
|
592
|
+
|
|
593
|
+
* **Phase B**: scoped built-ins are enabled, but they query only the frozen snapshot
|
|
594
|
+
|
|
595
|
+
* Eyeling runs saturation again (new facts can appear due to scoped queries)
|
|
596
|
+
|
|
597
|
+
This produces deterministic behavior for scoped operations: they observe a stable snapshot, not a moving target.
|
|
598
|
+
|
|
599
|
+
### 10.2 Priority-gated closure levels
|
|
600
|
+
|
|
601
|
+
Eyeling introduces a `scopedClosureLevel` counter:
|
|
602
|
+
|
|
603
|
+
* level 0 means “no snapshot available” (Phase A)
|
|
604
|
+
* level 1, 2, … correspond to snapshots produced after each Phase A saturation
|
|
605
|
+
|
|
606
|
+
Some built-ins interpret a positive integer literal as a requested priority:
|
|
607
|
+
|
|
608
|
+
* `log:collectAllIn` and `log:forAllIn` use the **object position** for priority
|
|
609
|
+
* `log:includes` and `log:notIncludes` use the **subject position** for priority
|
|
610
|
+
|
|
611
|
+
If a rule requests priority `N`, Eyeling delays that builtin until `scopedClosureLevel >= N`.
|
|
612
|
+
|
|
613
|
+
In practice this allows rule authors to write “don’t run this scoped query until the closure is stable enough” and is what lets Eyeling iterate safely when rule-producing rules introduce new needs.
|
|
614
|
+
|
|
615
|
+
### 10.3 `log:conclusion`: local deductive closure of a formula
|
|
616
|
+
|
|
617
|
+
`log:conclusion` is handled in a particularly elegant way:
|
|
618
|
+
|
|
619
|
+
* given a formula `{ ... }` (a `GraphTerm`),
|
|
620
|
+
* Eyeling computes the deductive closure *inside that formula*:
|
|
621
|
+
|
|
622
|
+
* extract rule triples inside it (`log:implies`, `log:impliedBy`)
|
|
623
|
+
* run `forwardChain` locally over those triples
|
|
624
|
+
* cache the result in a `WeakMap` so the same formula doesn’t get recomputed
|
|
625
|
+
|
|
626
|
+
Notably, `log:impliedBy` inside the formula is treated as forward implication too for closure computation (and also indexed as backward to help proving).
|
|
627
|
+
|
|
628
|
+
This makes formulas a little world you can reason about as data.
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
<a id="ch11"></a>
|
|
633
|
+
## Chapter 11 — Built-ins as a standard library (`evalBuiltin`)
|
|
634
|
+
|
|
635
|
+
Built-ins are where Eyeling stops being “just a Datalog engine” and becomes a practical N3 tool.
|
|
636
|
+
|
|
637
|
+
### 11.1 How Eyeling recognizes built-ins
|
|
638
|
+
|
|
639
|
+
A predicate is treated as builtin if:
|
|
640
|
+
|
|
641
|
+
* it is an IRI in one of the builtin namespaces:
|
|
642
|
+
|
|
643
|
+
* `crypto:`, `math:`, `log:`, `string:`, `time:`, `list:`
|
|
644
|
+
* or it is `rdf:first` / `rdf:rest` (treated as list-like builtins)
|
|
645
|
+
* unless **super restricted mode** is enabled, in which case only `log:implies` and `log:impliedBy` are treated as builtins.
|
|
646
|
+
|
|
647
|
+
Super restricted mode exists to let you treat all other predicates as ordinary facts/rules without any built-in evaluation.
|
|
648
|
+
|
|
649
|
+
### 11.2 Built-ins return multiple solutions
|
|
650
|
+
|
|
651
|
+
Every builtin returns a list of substitution *deltas*.
|
|
652
|
+
|
|
653
|
+
That means built-ins can be:
|
|
654
|
+
|
|
655
|
+
* **functional** (return one delta binding an output)
|
|
656
|
+
* **tests** (return either `[{}]` for success or `[]` for failure)
|
|
657
|
+
* **generators** (return many deltas)
|
|
658
|
+
|
|
659
|
+
List operations are a common source of generators; numeric comparisons are tests.
|
|
660
|
+
|
|
661
|
+
Below is a drop-in replacement for **§11.3 “A tour of builtin families”** that aims to be *fully self-contained* and to cover **every builtin currently implemented in `lib/engine.js`** (including the `rdf:first` / `rdf:rest` aliases).
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## 11.3 A tour of builtin families
|
|
666
|
+
|
|
667
|
+
Eyeling’s builtins are best thought of as *foreign predicates*: they look like ordinary N3 predicates in your rules, but when the engine tries to satisfy a goal whose predicate is a builtin, it does not search the fact store. Instead, it calls a piece of JavaScript that implements the predicate’s semantics.
|
|
668
|
+
|
|
669
|
+
That one sentence explains a lot of “why does it behave like *that*?”:
|
|
670
|
+
|
|
671
|
+
* Builtins are evaluated **during backward proof** (goal solving), just like facts and backward rules.
|
|
672
|
+
* A builtin may produce **zero solutions** (fail), **one solution** (deterministic succeed), or **many solutions** (a generator).
|
|
673
|
+
* Most builtins behave like relations, not like functions: they can sometimes run “backwards” (bind the subject from the object) if the implementation supports it.
|
|
674
|
+
* Some builtins are **pure tests** (constraints): they never introduce new bindings; they only succeed or fail. Eyeling recognizes a subset of these and tends to schedule them *late* in forward-rule premises so they run after other goals have had a chance to bind variables.
|
|
675
|
+
|
|
676
|
+
### 11.3.0 Reading builtin “signatures” in this handbook
|
|
677
|
+
|
|
678
|
+
The N3 Builtins tradition often describes builtins using “schema” annotations like:
|
|
679
|
+
|
|
680
|
+
* `$s+` / `$o+` — input must be bound (or at least not a variable in practice)
|
|
681
|
+
* `$s-` / `$o-` — output position (often a variable that will be bound)
|
|
682
|
+
* `$s?` / `$o?` — may be unbound
|
|
683
|
+
* `$s.i` — list element *i* inside the subject list
|
|
684
|
+
|
|
685
|
+
Eyeling is a little more pragmatic: it implements the spirit of these schemas, but it also has several “engineering” conventions that appear across many builtins:
|
|
686
|
+
|
|
687
|
+
1. **Variables (`?X`) may be bound** by a builtin if the builtin is written to do so.
|
|
688
|
+
2. **Blank nodes (`[]` / `_:`)** are frequently treated as “don’t care” placeholders. Many builtins accept a blank node in an output position and simply succeed without binding.
|
|
689
|
+
3. **Fully unbound relations are usually not enumerated.** If both sides are unbound and enumerating solutions would be infinite (or huge), a number of builtins treat that situation as “satisfiable” and succeed once without binding anything. (This is mainly to keep meta-tests and some N3 conformance cases happy.)
|
|
690
|
+
|
|
691
|
+
With that, we can tour the builtin families as Eyeling actually implements them.
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
## 11.3.1 `crypto:` — digest functions (Node-only)
|
|
696
|
+
|
|
697
|
+
These builtins hash a string and return a lowercase hex digest as a plain string literal.
|
|
698
|
+
|
|
699
|
+
### `crypto:sha`, `crypto:md5`, `crypto:sha256`, `crypto:sha512`
|
|
700
|
+
|
|
701
|
+
**Shape:**
|
|
702
|
+
`$literal crypto:sha256 $digest`
|
|
703
|
+
|
|
704
|
+
**Semantics (Eyeling):**
|
|
705
|
+
|
|
706
|
+
* The **subject must be a literal**. Eyeling takes the literal’s lexical form (stripping quotes) as UTF-8 input.
|
|
707
|
+
* The **object** is unified with a **plain string literal** containing the hex digest.
|
|
708
|
+
|
|
709
|
+
**Important runtime note:** Eyeling uses Node’s `crypto` module. If `crypto` is not available (e.g., in some browser builds), these builtins simply **fail** (return no solutions).
|
|
710
|
+
|
|
711
|
+
**Example:**
|
|
712
|
+
|
|
713
|
+
```n3
|
|
714
|
+
"hello" crypto:sha256 ?d.
|
|
715
|
+
# ?d becomes "2cf24dba5...<snip>...9824"
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## 11.3.2 `math:` — numeric and numeric-like relations
|
|
721
|
+
|
|
722
|
+
Eyeling’s `math:` builtins fall into three broad categories:
|
|
723
|
+
|
|
724
|
+
1. **Comparisons**: constraint-style predicates (`>`, `<`, `=`, …).
|
|
725
|
+
2. **Arithmetic on numbers**: sums, products, division, rounding, etc.
|
|
726
|
+
3. **Unary analytic functions**: trig/hyperbolic functions and a few helpers.
|
|
727
|
+
|
|
728
|
+
A key design choice: Eyeling parses numeric terms fairly strictly, but comparisons accept a wider “numeric-like” domain including durations and date/time values in some cases.
|
|
729
|
+
|
|
730
|
+
### 11.3.2.1 Numeric comparisons (constraints)
|
|
731
|
+
|
|
732
|
+
These builtins succeed or fail; they do not introduce new bindings.
|
|
733
|
+
|
|
734
|
+
* `math:greaterThan` (>)
|
|
735
|
+
* `math:lessThan` (<)
|
|
736
|
+
* `math:notGreaterThan` (≤)
|
|
737
|
+
* `math:notLessThan` (≥)
|
|
738
|
+
* `math:equalTo` (=)
|
|
739
|
+
* `math:notEqualTo` (≠)
|
|
740
|
+
|
|
741
|
+
**Shapes:**
|
|
742
|
+
|
|
743
|
+
```n3
|
|
744
|
+
$a math:greaterThan $b.
|
|
745
|
+
$a math:equalTo $b.
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Eyeling also accepts an older cwm-ish variant where the **subject is a 2-element list**:
|
|
749
|
+
|
|
750
|
+
```n3
|
|
751
|
+
( $a $b ) math:greaterThan true. # (supported as a convenience)
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Accepted term types (Eyeling):**
|
|
755
|
+
|
|
756
|
+
* Proper XSD numeric literals (`xsd:integer`, `xsd:decimal`, `xsd:float`, `xsd:double`, and integer-derived types).
|
|
757
|
+
* Untyped numeric tokens (`123`, `-4.5`, `1.2e3`) when they look numeric.
|
|
758
|
+
* `xsd:duration` literals (treated as seconds via a simplified model).
|
|
759
|
+
* `xsd:date` and `xsd:dateTime` literals (converted to epoch seconds for comparison).
|
|
760
|
+
|
|
761
|
+
**Edge cases:**
|
|
762
|
+
|
|
763
|
+
* `NaN` is treated as **not equal to anything**, including itself, for `math:equalTo`.
|
|
764
|
+
* Comparisons involving non-parsable values simply fail.
|
|
765
|
+
|
|
766
|
+
Because these are pure tests, Eyeling treats them as **constraint builtins** and tends to push them to the end of forward-rule premises so they’re checked after other goals bind variables.
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
### 11.3.2.2 Arithmetic on lists of numbers
|
|
771
|
+
|
|
772
|
+
These are “function-like” relations where the subject is usually a list and the object is the result.
|
|
773
|
+
|
|
774
|
+
#### `math:sum`
|
|
775
|
+
|
|
776
|
+
**Shape:** `( $x1 $x2 ... ) math:sum $total`
|
|
777
|
+
|
|
778
|
+
* Subject must be a list of **at least two** numeric terms.
|
|
779
|
+
* Computes the numeric sum.
|
|
780
|
+
* Chooses an output datatype based on the “widest” numeric datatype seen among inputs and (optionally) the object position; integers stay integers unless the result is non-integer.
|
|
781
|
+
|
|
782
|
+
#### `math:product`
|
|
783
|
+
|
|
784
|
+
**Shape:** `( $x1 $x2 ... ) math:product $total`
|
|
785
|
+
|
|
786
|
+
* Same conventions as `math:sum`, but multiplies.
|
|
787
|
+
|
|
788
|
+
#### `math:difference`
|
|
789
|
+
|
|
790
|
+
This one is more interesting because Eyeling supports a couple of mixed “numeric-like” cases.
|
|
791
|
+
|
|
792
|
+
**Shape:** `( $a $b ) math:difference $c`
|
|
793
|
+
|
|
794
|
+
Eyeling supports:
|
|
795
|
+
|
|
796
|
+
1. **Numeric subtraction**: `c = a - b`.
|
|
797
|
+
2. **DateTime difference**: `(dateTime1 dateTime2) math:difference duration`
|
|
798
|
+
|
|
799
|
+
* Produces an `xsd:duration` in whole days (internally computed via seconds then formatted).
|
|
800
|
+
3. **DateTime minus duration**: `(dateTime duration) math:difference dateTime`
|
|
801
|
+
|
|
802
|
+
* Subtracts a duration from a dateTime and yields a new dateTime.
|
|
803
|
+
|
|
804
|
+
If the types don’t fit any supported case, the builtin fails.
|
|
805
|
+
|
|
806
|
+
#### `math:quotient`
|
|
807
|
+
|
|
808
|
+
**Shape:** `( $a $b ) math:quotient $q`
|
|
809
|
+
|
|
810
|
+
* Parses both inputs as numbers.
|
|
811
|
+
* Requires finite values and `b != 0`.
|
|
812
|
+
* Computes `a / b`, picking a suitable numeric datatype for output.
|
|
813
|
+
|
|
814
|
+
#### `math:integerQuotient`
|
|
815
|
+
|
|
816
|
+
**Shape:** `( $a $b ) math:integerQuotient $q`
|
|
817
|
+
|
|
818
|
+
* Intended for integer division with remainder discarded (truncation toward zero).
|
|
819
|
+
* Prefers exact arithmetic using **BigInt** if both inputs are integer literals.
|
|
820
|
+
* Falls back to Number parsing if needed, but still requires integer-like values.
|
|
821
|
+
|
|
822
|
+
#### `math:remainder`
|
|
823
|
+
|
|
824
|
+
**Shape:** `( $a $b ) math:remainder $r`
|
|
825
|
+
|
|
826
|
+
* Integer-only modulus.
|
|
827
|
+
* Uses BigInt when possible; otherwise requires both numbers to still represent integers.
|
|
828
|
+
* Fails on division by zero.
|
|
829
|
+
|
|
830
|
+
#### `math:rounded`
|
|
831
|
+
|
|
832
|
+
**Shape:** `$x math:rounded $n`
|
|
833
|
+
|
|
834
|
+
* Rounds to nearest integer.
|
|
835
|
+
* Tie-breaking follows JavaScript `Math.round`, i.e. halves go toward **+∞** (`-1.5 -> -1`, `1.5 -> 2`).
|
|
836
|
+
* Eyeling emits the integer as an **integer token literal** (and also accepts typed numerics if they compare equal).
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
### 11.3.2.3 Exponentiation and unary numeric relations
|
|
841
|
+
|
|
842
|
+
#### `math:exponentiation`
|
|
843
|
+
|
|
844
|
+
**Shape:** `( $base $exp ) math:exponentiation $result`
|
|
845
|
+
|
|
846
|
+
* Forward direction: if base and exponent are numeric, computes `base ** exp`.
|
|
847
|
+
* Reverse direction (limited): Eyeling can sometimes solve for the exponent if:
|
|
848
|
+
|
|
849
|
+
* base and result are numeric, finite, and **positive**
|
|
850
|
+
* base is not 1
|
|
851
|
+
* exponent is unbound
|
|
852
|
+
In that case it uses logarithms: `exp = log(result) / log(base)`.
|
|
853
|
+
|
|
854
|
+
This is a pragmatic inversion, not a full algebra system.
|
|
855
|
+
|
|
856
|
+
#### Unary “math relations” (often invertible)
|
|
857
|
+
|
|
858
|
+
Eyeling implements these as a shared pattern: if the subject is numeric, compute object; else if the object is numeric, compute subject via an inverse function; if both sides are unbound, succeed once (don’t enumerate).
|
|
859
|
+
|
|
860
|
+
* `math:absoluteValue`
|
|
861
|
+
* `math:negation`
|
|
862
|
+
* `math:degrees` (and implicitly its inverse “radians” conversion)
|
|
863
|
+
* `math:sin`, `math:cos`, `math:tan`
|
|
864
|
+
* `math:asin`, `math:acos`, `math:atan`
|
|
865
|
+
* `math:sinh`, `math:cosh`, `math:tanh` (only if JS provides the functions)
|
|
866
|
+
|
|
867
|
+
**Example:**
|
|
868
|
+
|
|
869
|
+
```n3
|
|
870
|
+
"0"^^xsd:double math:cos ?c. # forward
|
|
871
|
+
?x math:cos "1"^^xsd:double. # reverse (principal acos)
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Inversion uses principal values (e.g., `asin`, `acos`, `atan`) and does not attempt to enumerate periodic families of solutions.
|
|
875
|
+
|
|
876
|
+
---
|
|
877
|
+
|
|
878
|
+
## 11.3.3 `time:` — dateTime inspection and “now”
|
|
879
|
+
|
|
880
|
+
Eyeling’s time builtins work over `xsd:dateTime` lexical forms. They are deliberately simple: they extract components from the lexical form rather than implementing a full time zone database.
|
|
881
|
+
|
|
882
|
+
### Component extractors
|
|
883
|
+
|
|
884
|
+
* `time:year`
|
|
885
|
+
* `time:month`
|
|
886
|
+
* `time:day`
|
|
887
|
+
* `time:hour`
|
|
888
|
+
* `time:minute`
|
|
889
|
+
* `time:second`
|
|
890
|
+
|
|
891
|
+
**Shape:**
|
|
892
|
+
`$dt time:month $m`
|
|
893
|
+
|
|
894
|
+
**Semantics:**
|
|
895
|
+
|
|
896
|
+
* Subject must be an `xsd:dateTime` literal in a format Eyeling can parse.
|
|
897
|
+
* Object becomes the corresponding integer component (as an integer token literal).
|
|
898
|
+
* If the object is already a numeric literal, Eyeling accepts it if it matches.
|
|
899
|
+
|
|
900
|
+
### `time:timeZone`
|
|
901
|
+
|
|
902
|
+
**Shape:**
|
|
903
|
+
`$dt time:timeZone $tz`
|
|
904
|
+
|
|
905
|
+
Returns the trailing zone designator:
|
|
906
|
+
|
|
907
|
+
* `"Z"` for UTC, or
|
|
908
|
+
* a string like `"+02:00"` / `"-05:00"`
|
|
909
|
+
|
|
910
|
+
It yields a **plain string literal** (and also accepts typed `xsd:string` literals).
|
|
911
|
+
|
|
912
|
+
### `time:localTime`
|
|
913
|
+
|
|
914
|
+
**Shape:**
|
|
915
|
+
`"" time:localTime ?now`
|
|
916
|
+
|
|
917
|
+
Binds `?now` to the current local time as an `xsd:dateTime` literal.
|
|
918
|
+
|
|
919
|
+
Two subtle but important engineering choices:
|
|
920
|
+
|
|
921
|
+
1. Eyeling memoizes “now” per reasoning run so that repeated uses in one run don’t drift.
|
|
922
|
+
2. Eyeling supports a fixed “now” override (used for deterministic tests).
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 11.3.4 `list:` — list structure, iteration, and higher-order helpers
|
|
927
|
+
|
|
928
|
+
Eyeling has a real internal list term (`ListTerm`) that corresponds to N3’s `(a b c)` surface syntax.
|
|
929
|
+
|
|
930
|
+
### RDF collections (`rdf:first` / `rdf:rest`) are materialized
|
|
931
|
+
|
|
932
|
+
N3 and RDF can also express lists as linked blank nodes using `rdf:first` / `rdf:rest` and `rdf:nil`. Eyeling *materializes* such structures into internal list terms before reasoning so that `list:*` builtins can operate uniformly.
|
|
933
|
+
|
|
934
|
+
For convenience and compatibility, Eyeling treats:
|
|
935
|
+
|
|
936
|
+
* `rdf:first` as an alias of `list:first`
|
|
937
|
+
* `rdf:rest` as an alias of `list:rest`
|
|
938
|
+
|
|
939
|
+
### Core list destructuring
|
|
940
|
+
|
|
941
|
+
#### `list:first` (and `rdf:first`)
|
|
942
|
+
|
|
943
|
+
**Shape:**
|
|
944
|
+
`(a b c) list:first a`
|
|
945
|
+
|
|
946
|
+
* Succeeds iff the subject is a **non-empty closed list**.
|
|
947
|
+
* Unifies the object with the first element.
|
|
948
|
+
|
|
949
|
+
#### `list:rest` (and `rdf:rest`)
|
|
950
|
+
|
|
951
|
+
**Shape:**
|
|
952
|
+
`(a b c) list:rest (b c)`
|
|
953
|
+
|
|
954
|
+
Eyeling supports both:
|
|
955
|
+
|
|
956
|
+
* closed lists `(a b c)`, and
|
|
957
|
+
* *open lists* of the form `(a b ... ?T)` internally.
|
|
958
|
+
|
|
959
|
+
For open lists, “rest” preserves openness:
|
|
960
|
+
|
|
961
|
+
* Rest of `(a ... ?T)` is `?T`
|
|
962
|
+
* Rest of `(a b ... ?T)` is `(b ... ?T)`
|
|
963
|
+
|
|
964
|
+
#### `list:firstRest`
|
|
965
|
+
|
|
966
|
+
This is a very useful “paired” view of a list.
|
|
967
|
+
|
|
968
|
+
**Forward shape:**
|
|
969
|
+
`(a b c) list:firstRest (a (b c))`
|
|
970
|
+
|
|
971
|
+
**Backward shapes (construction):**
|
|
972
|
+
|
|
973
|
+
* If the object is `(first restList)`, it can construct the list.
|
|
974
|
+
* If `rest` is a variable, Eyeling constructs an open list term.
|
|
975
|
+
|
|
976
|
+
This is the closest thing to Prolog’s `[H|T]` in Eyeling.
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
### Membership and iteration (multi-solution builtins)
|
|
981
|
+
|
|
982
|
+
These builtins can yield multiple solutions.
|
|
983
|
+
|
|
984
|
+
#### `list:member`
|
|
985
|
+
|
|
986
|
+
**Shape:**
|
|
987
|
+
`(a b c) list:member ?x`
|
|
988
|
+
|
|
989
|
+
Generates one solution per element, unifying the object with each member.
|
|
990
|
+
|
|
991
|
+
#### `list:in`
|
|
992
|
+
|
|
993
|
+
**Shape:**
|
|
994
|
+
`?x list:in (a b c)`
|
|
995
|
+
|
|
996
|
+
Same idea, but the list is in the **object** position and the **subject** is unified with each element.
|
|
997
|
+
|
|
998
|
+
#### `list:iterate`
|
|
999
|
+
|
|
1000
|
+
**Shape:**
|
|
1001
|
+
`(a b c) list:iterate ?pair`
|
|
1002
|
+
|
|
1003
|
+
Generates `(index value)` pairs with **0-based indices**:
|
|
1004
|
+
|
|
1005
|
+
* `(0 a)`, `(1 b)`, `(2 c)`, …
|
|
1006
|
+
|
|
1007
|
+
A nice ergonomic detail: the object may be a pattern such as:
|
|
1008
|
+
|
|
1009
|
+
```n3
|
|
1010
|
+
(a b c) list:iterate ( ?i "b" ).
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
In that case Eyeling unifies `?i` with `1` and checks the value part appropriately.
|
|
1014
|
+
|
|
1015
|
+
#### `list:memberAt`
|
|
1016
|
+
|
|
1017
|
+
**Shape:**
|
|
1018
|
+
`( (a b c) 1 ) list:memberAt b`
|
|
1019
|
+
|
|
1020
|
+
The subject must be a 2-element list: `(listTerm indexTerm)`.
|
|
1021
|
+
|
|
1022
|
+
Eyeling can use this relationally:
|
|
1023
|
+
|
|
1024
|
+
* If the index is bound, it can return the value.
|
|
1025
|
+
* If the value is bound, it can search for indices that match.
|
|
1026
|
+
* If both are variables, it generates pairs (similar to `iterate`, but with separate index/value logic).
|
|
1027
|
+
|
|
1028
|
+
Indices are **0-based**.
|
|
1029
|
+
|
|
1030
|
+
---
|
|
1031
|
+
|
|
1032
|
+
### Transformations and queries
|
|
1033
|
+
|
|
1034
|
+
#### `list:length`
|
|
1035
|
+
|
|
1036
|
+
**Shape:**
|
|
1037
|
+
`(a b c) list:length 3`
|
|
1038
|
+
|
|
1039
|
+
Returns the length as an integer token literal.
|
|
1040
|
+
|
|
1041
|
+
A small but intentional strictness: if the object is already ground, Eyeling does not accept “integer vs decimal equivalences” here; it wants the exact integer notion.
|
|
1042
|
+
|
|
1043
|
+
#### `list:last`
|
|
1044
|
+
|
|
1045
|
+
**Shape:**
|
|
1046
|
+
`(a b c) list:last c`
|
|
1047
|
+
|
|
1048
|
+
Returns the last element of a non-empty list.
|
|
1049
|
+
|
|
1050
|
+
#### `list:reverse`
|
|
1051
|
+
|
|
1052
|
+
Reversible in the sense that either side may be the list:
|
|
1053
|
+
|
|
1054
|
+
* If subject is a list, object becomes its reversal.
|
|
1055
|
+
* If object is a list, subject becomes its reversal.
|
|
1056
|
+
|
|
1057
|
+
It does not enumerate arbitrary reversals; it’s a deterministic transform once one side is known.
|
|
1058
|
+
|
|
1059
|
+
#### `list:remove`
|
|
1060
|
+
|
|
1061
|
+
**Shape:**
|
|
1062
|
+
`( (a b a c) a ) list:remove (b c)`
|
|
1063
|
+
|
|
1064
|
+
Removes all occurrences of an item from a list.
|
|
1065
|
+
|
|
1066
|
+
Important constraint: the item to remove must be **ground** (fully known) before the builtin will run.
|
|
1067
|
+
|
|
1068
|
+
#### `list:notMember` (constraint)
|
|
1069
|
+
|
|
1070
|
+
**Shape:**
|
|
1071
|
+
`(a b c) list:notMember x`
|
|
1072
|
+
|
|
1073
|
+
Succeeds iff the object cannot be unified with any element of the subject list. This is treated as a constraint builtin.
|
|
1074
|
+
|
|
1075
|
+
#### `list:append`
|
|
1076
|
+
|
|
1077
|
+
This is list concatenation, but Eyeling implements it in a pleasantly relational way.
|
|
1078
|
+
|
|
1079
|
+
**Forward shape:**
|
|
1080
|
+
`( (a b) (c) (d e) ) list:append (a b c d e)`
|
|
1081
|
+
|
|
1082
|
+
Subject is a list of lists; object is their concatenation.
|
|
1083
|
+
|
|
1084
|
+
**Splitting (reverse-ish) mode:**
|
|
1085
|
+
If the **object is a concrete list**, Eyeling tries all ways of splitting it into the given number of parts and unifying each part with the corresponding subject element. This can yield multiple solutions and is handy for logic programming patterns.
|
|
1086
|
+
|
|
1087
|
+
#### `list:sort`
|
|
1088
|
+
|
|
1089
|
+
Sorts a list into a deterministic order.
|
|
1090
|
+
|
|
1091
|
+
* Requires the input list’s elements to be **ground**.
|
|
1092
|
+
* Orders literals numerically when both sides look numeric; otherwise compares their lexical strings.
|
|
1093
|
+
* Orders lists lexicographically by elements.
|
|
1094
|
+
* Orders IRIs by IRI string.
|
|
1095
|
+
* Falls back to a stable structural key for mixed cases.
|
|
1096
|
+
|
|
1097
|
+
Like `reverse`, this is “reversible” only in the sense that if one side is a list, the other side can be unified with its sorted form.
|
|
1098
|
+
|
|
1099
|
+
#### `list:map` (higher-order)
|
|
1100
|
+
|
|
1101
|
+
This is one of Eyeling’s most powerful list builtins because it calls back into the reasoner.
|
|
1102
|
+
|
|
1103
|
+
**Shape:**
|
|
1104
|
+
`( (x1 x2 x3) ex:pred ) list:map ?outList`
|
|
1105
|
+
|
|
1106
|
+
Semantics:
|
|
1107
|
+
|
|
1108
|
+
1. The subject is a 2-element list: `(inputList predicateIri)`.
|
|
1109
|
+
2. `inputList` must be ground.
|
|
1110
|
+
3. For each element `el` in the input list, Eyeling proves the goal:
|
|
1111
|
+
|
|
1112
|
+
```n3
|
|
1113
|
+
el predicateIri ?y.
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
using *the full engine* (facts, backward rules, and builtins).
|
|
1117
|
+
4. All resulting `?y` values are collected in proof order and concatenated into the output list.
|
|
1118
|
+
5. If an element produces no solutions, it contributes nothing.
|
|
1119
|
+
|
|
1120
|
+
This makes `list:map` a compact “query over a list” operator.
|
|
1121
|
+
|
|
1122
|
+
---
|
|
1123
|
+
|
|
1124
|
+
## 11.3.5 `log:` — unification, formulas, scoping, and meta-level control
|
|
1125
|
+
|
|
1126
|
+
The `log:` family is where N3 stops being “RDF with rules” and becomes a *meta-logic*. Eyeling supports the core operators you need to treat formulas as terms, reason inside quoted graphs, and compute closures.
|
|
1127
|
+
|
|
1128
|
+
### Equality and inequality
|
|
1129
|
+
|
|
1130
|
+
#### `log:equalTo`
|
|
1131
|
+
|
|
1132
|
+
**Shape:**
|
|
1133
|
+
`$x log:equalTo $y`
|
|
1134
|
+
|
|
1135
|
+
This is simply **term unification**: it succeeds if the two terms can be unified and returns any bindings that result.
|
|
1136
|
+
|
|
1137
|
+
#### `log:notEqualTo` (constraint)
|
|
1138
|
+
|
|
1139
|
+
Succeeds iff the terms **cannot** be unified. No new bindings.
|
|
1140
|
+
|
|
1141
|
+
### Working with formulas as terms
|
|
1142
|
+
|
|
1143
|
+
In Eyeling, a quoted formula `{ ... }` is represented as a `GraphTerm` whose content is a list of triples (and, when parsed from documents, rule terms can also appear as `log:implies` / `log:impliedBy` triples inside formulas).
|
|
1144
|
+
|
|
1145
|
+
#### `log:conjunction`
|
|
1146
|
+
|
|
1147
|
+
**Shape:**
|
|
1148
|
+
`( F1 F2 ... ) log:conjunction F`
|
|
1149
|
+
|
|
1150
|
+
* Subject is a list of formulas.
|
|
1151
|
+
* Object becomes a formula containing all triples from all inputs.
|
|
1152
|
+
* Duplicate triples are removed.
|
|
1153
|
+
* The literal `true` is treated as the **empty formula** and is ignored in the merge.
|
|
1154
|
+
|
|
1155
|
+
#### `log:conclusion`
|
|
1156
|
+
|
|
1157
|
+
**Shape:**
|
|
1158
|
+
`F log:conclusion C`
|
|
1159
|
+
|
|
1160
|
+
Computes the *deductive closure* of the formula `F` **using only the information inside `F`**:
|
|
1161
|
+
|
|
1162
|
+
* Eyeling starts with all triples inside `F` as facts.
|
|
1163
|
+
* It treats `{A} => {B}` (represented internally as a `log:implies` triple between formulas) as a forward rule.
|
|
1164
|
+
* It treats `{A} <= {B}` as the corresponding forward direction for closure purposes.
|
|
1165
|
+
* Then it forward-chains to a fixpoint *within that local fact set*.
|
|
1166
|
+
* The result is returned as a formula containing all derived triples.
|
|
1167
|
+
|
|
1168
|
+
Eyeling caches `log:conclusion` results per formula object, so repeated calls with the same formula term are cheap.
|
|
1169
|
+
|
|
1170
|
+
### Dereferencing and parsing (I/O flavored)
|
|
1171
|
+
|
|
1172
|
+
These builtins reach outside the current fact set. They are synchronous by design.
|
|
1173
|
+
|
|
1174
|
+
#### `log:content`
|
|
1175
|
+
|
|
1176
|
+
**Shape:**
|
|
1177
|
+
`<doc> log:content ?txt`
|
|
1178
|
+
|
|
1179
|
+
* Dereferences the IRI (fragment stripped) and returns the raw bytes as an `xsd:string` literal.
|
|
1180
|
+
* In Node: HTTP(S) is fetched synchronously; non-HTTP is treated as a local file path (including `file://`).
|
|
1181
|
+
* In browsers/workers: uses synchronous XHR (subject to CORS).
|
|
1182
|
+
|
|
1183
|
+
#### `log:semantics`
|
|
1184
|
+
|
|
1185
|
+
**Shape:**
|
|
1186
|
+
`<doc> log:semantics ?formula`
|
|
1187
|
+
|
|
1188
|
+
Dereferences and parses the remote/local resource as N3/Turtle-like syntax, returning a formula.
|
|
1189
|
+
|
|
1190
|
+
A nice detail: top-level rules in the parsed document are represented *as data* inside the returned formula using `log:implies` / `log:impliedBy` triples between formula terms. This means you can treat “a document plus its rules” as a single first-class formula object.
|
|
1191
|
+
|
|
1192
|
+
#### `log:semanticsOrError`
|
|
1193
|
+
|
|
1194
|
+
Like `log:semantics`, but on failure it returns a string literal such as:
|
|
1195
|
+
|
|
1196
|
+
* `error(dereference_failed,...)`
|
|
1197
|
+
* `error(parse_error,...)`
|
|
1198
|
+
|
|
1199
|
+
This is convenient in robust pipelines where you want logic that can react to failures.
|
|
1200
|
+
|
|
1201
|
+
#### `log:parsedAsN3`
|
|
1202
|
+
|
|
1203
|
+
**Shape:**
|
|
1204
|
+
`" ...n3 text... " log:parsedAsN3 ?formula`
|
|
1205
|
+
|
|
1206
|
+
Parses an in-memory string as N3 and returns the corresponding formula.
|
|
1207
|
+
|
|
1208
|
+
### Type inspection
|
|
1209
|
+
|
|
1210
|
+
#### `log:rawType`
|
|
1211
|
+
|
|
1212
|
+
Returns one of four IRIs:
|
|
1213
|
+
|
|
1214
|
+
* `log:Formula` (quoted graph)
|
|
1215
|
+
* `log:Literal`
|
|
1216
|
+
* `rdf:List` (closed or open list terms)
|
|
1217
|
+
* `log:Other` (IRIs, blank nodes, etc.)
|
|
1218
|
+
|
|
1219
|
+
### Literal constructors
|
|
1220
|
+
|
|
1221
|
+
These two are classic N3 “bridge” operators between structured data and concrete RDF literal forms.
|
|
1222
|
+
|
|
1223
|
+
#### `log:dtlit`
|
|
1224
|
+
|
|
1225
|
+
Relates a datatype literal to a pair `(lex datatypeIri)`.
|
|
1226
|
+
|
|
1227
|
+
* If object is a literal, it can produce the subject list `(stringLiteral datatypeIri)`.
|
|
1228
|
+
* If subject is such a list, it can produce the corresponding datatype literal.
|
|
1229
|
+
* If both subject and object are variables, Eyeling treats this as satisfiable and succeeds once.
|
|
1230
|
+
|
|
1231
|
+
Language-tagged strings are normalized: they are treated as having datatype `rdf:langString`.
|
|
1232
|
+
|
|
1233
|
+
#### `log:langlit`
|
|
1234
|
+
|
|
1235
|
+
Relates a language-tagged literal to a pair `(lex langTag)`.
|
|
1236
|
+
|
|
1237
|
+
* If object is `"hello"@en`, subject can become `("hello" "en")`.
|
|
1238
|
+
* If subject is `("hello" "en")`, object can become `"hello"@en`.
|
|
1239
|
+
* Fully unbound succeeds once.
|
|
1240
|
+
|
|
1241
|
+
### Rules as data: introspection
|
|
1242
|
+
|
|
1243
|
+
#### `log:implies` and `log:impliedBy`
|
|
1244
|
+
|
|
1245
|
+
As *syntax*, Eyeling parses `{A} => {B}` and `{A} <= {B}` into internal forward/backward rules.
|
|
1246
|
+
|
|
1247
|
+
As *builtins*, `log:implies` and `log:impliedBy` let you **inspect the currently loaded rule set**:
|
|
1248
|
+
|
|
1249
|
+
* `log:implies` enumerates forward rules as `(premiseFormula, conclusionFormula)` pairs.
|
|
1250
|
+
* `log:impliedBy` enumerates backward rules similarly.
|
|
1251
|
+
|
|
1252
|
+
Each enumerated rule is standardized apart (fresh variable names) before unification so you can safely query over it.
|
|
1253
|
+
|
|
1254
|
+
### Scoped proof inside formulas: `log:includes` and friends
|
|
1255
|
+
|
|
1256
|
+
#### `log:includes`
|
|
1257
|
+
|
|
1258
|
+
**Shape:**
|
|
1259
|
+
`Scope log:includes GoalFormula`
|
|
1260
|
+
|
|
1261
|
+
This proves all triples in `GoalFormula` as goals, returning the substitutions that make them provable.
|
|
1262
|
+
|
|
1263
|
+
Eyeling has **two modes**:
|
|
1264
|
+
|
|
1265
|
+
1. **Explicit scope graph**: if `Scope` is a formula `{...}`
|
|
1266
|
+
|
|
1267
|
+
* Eyeling reasons *only inside that formula* (its triples are the fact store).
|
|
1268
|
+
* External rules are not used.
|
|
1269
|
+
|
|
1270
|
+
2. **Priority-gated global scope**: otherwise
|
|
1271
|
+
|
|
1272
|
+
* Eyeling uses a *frozen snapshot* of the current global closure.
|
|
1273
|
+
* The “priority” is read from the subject if it’s a positive integer literal `N`.
|
|
1274
|
+
* If the closure level is below `N`, the builtin “delays” by failing at that point in the search.
|
|
1275
|
+
|
|
1276
|
+
This priority mechanism exists because Eyeling’s forward chaining runs in outer iterations with a “freeze snapshot then evaluate scoped builtins” phase. The goal is to make scoped meta-builtins stable and deterministic: they query a fixed snapshot rather than chasing a fact store that is being mutated mid-iteration.
|
|
1277
|
+
|
|
1278
|
+
Also supported:
|
|
1279
|
+
|
|
1280
|
+
* The object may be the literal `true`, meaning the empty formula, which is always included (subject to the priority gating above).
|
|
1281
|
+
|
|
1282
|
+
#### `log:notIncludes` (constraint)
|
|
1283
|
+
|
|
1284
|
+
Negation-as-failure version: it succeeds iff `log:includes` would yield no solutions (under the same scoping rules).
|
|
1285
|
+
|
|
1286
|
+
#### `log:collectAllIn`
|
|
1287
|
+
|
|
1288
|
+
**Shape:**
|
|
1289
|
+
`( ValueTemplate WhereFormula OutList ) log:collectAllIn Scope`
|
|
1290
|
+
|
|
1291
|
+
* Proves `WhereFormula` in the chosen scope.
|
|
1292
|
+
* For each solution, applies it to `ValueTemplate` and collects the instantiated terms into a list.
|
|
1293
|
+
* Unifies `OutList` with that list.
|
|
1294
|
+
* If `OutList` is a blank node, Eyeling just checks satisfiable without binding/collecting.
|
|
1295
|
+
|
|
1296
|
+
This is essentially a list-producing “findall”.
|
|
1297
|
+
|
|
1298
|
+
#### `log:forAllIn` (constraint)
|
|
1299
|
+
|
|
1300
|
+
**Shape:**
|
|
1301
|
+
`( WhereFormula ThenFormula ) log:forAllIn Scope`
|
|
1302
|
+
|
|
1303
|
+
For every solution of `WhereFormula`, `ThenFormula` must be provable under the bindings of that solution. If any witness fails, the builtin fails. No bindings are returned.
|
|
1304
|
+
|
|
1305
|
+
This is treated as a constraint builtin.
|
|
1306
|
+
|
|
1307
|
+
### Skolemization and URI casting
|
|
1308
|
+
|
|
1309
|
+
#### `log:skolem`
|
|
1310
|
+
|
|
1311
|
+
**Shape:**
|
|
1312
|
+
`$groundTerm log:skolem ?iri`
|
|
1313
|
+
|
|
1314
|
+
Deterministically maps a *ground* term to a Skolem IRI in Eyeling’s well-known namespace. This is extremely useful when you want a repeatable identifier derived from structured content.
|
|
1315
|
+
|
|
1316
|
+
#### `log:uri`
|
|
1317
|
+
|
|
1318
|
+
Bidirectional conversion between IRIs and their string form:
|
|
1319
|
+
|
|
1320
|
+
* If subject is an IRI, object can be unified with a string literal of its IRI.
|
|
1321
|
+
* If object is a string literal, subject can be unified with the corresponding IRI — **but** Eyeling rejects strings that cannot be safely serialized as `<...>` in Turtle/N3, and it rejects `_:`-style strings to avoid confusing blank nodes with IRIs.
|
|
1322
|
+
* Some “fully unbound / don’t-care” combinations succeed once to avoid infinite enumeration.
|
|
1323
|
+
|
|
1324
|
+
### Side effects and output directives
|
|
1325
|
+
|
|
1326
|
+
#### `log:trace`
|
|
1327
|
+
|
|
1328
|
+
Always succeeds once and prints a debug line to stderr:
|
|
1329
|
+
|
|
1330
|
+
```
|
|
1331
|
+
<s> TRACE <o>
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
using the current prefix environment for pretty printing.
|
|
1335
|
+
|
|
1336
|
+
#### `log:outputString`
|
|
1337
|
+
|
|
1338
|
+
As a goal, this builtin simply checks that the terms are sufficiently bound/usable and then succeeds. The actual “printing” behavior is handled by the CLI:
|
|
1339
|
+
|
|
1340
|
+
* When you run Eyeling with `--strings` / `-r`, the CLI collects all `log:outputString` triples from the *saturated* closure.
|
|
1341
|
+
* It sorts them deterministically by the subject “key” and concatenates the string values in that order.
|
|
1342
|
+
|
|
1343
|
+
This is treated as a constraint builtin (it shouldn’t drive search; it should merely validate that strings exist once other reasoning has produced them).
|
|
1344
|
+
|
|
1345
|
+
---
|
|
1346
|
+
|
|
1347
|
+
## 11.3.6 `string:` — string casting, tests, regexes, and JSON pointers
|
|
1348
|
+
|
|
1349
|
+
Eyeling implements string builtins with a deliberate interpretation of “domain is `xsd:string`”:
|
|
1350
|
+
|
|
1351
|
+
* Any **IRI** can be cast to a string (its IRI text).
|
|
1352
|
+
* Any **literal** can be cast to a string:
|
|
1353
|
+
|
|
1354
|
+
* quoted lexical forms decode N3/Turtle escapes,
|
|
1355
|
+
* unquoted lexical tokens are taken as-is (numbers, booleans, dateTimes, …).
|
|
1356
|
+
* Blank nodes, lists, formulas, and variables are not string-castable (and cause the builtin to fail).
|
|
1357
|
+
|
|
1358
|
+
### Construction and concatenation
|
|
1359
|
+
|
|
1360
|
+
#### `string:concatenation`
|
|
1361
|
+
|
|
1362
|
+
**Shape:**
|
|
1363
|
+
`( s1 s2 ... ) string:concatenation s`
|
|
1364
|
+
|
|
1365
|
+
Casts each element to a string and concatenates.
|
|
1366
|
+
|
|
1367
|
+
#### `string:format`
|
|
1368
|
+
|
|
1369
|
+
**Shape:**
|
|
1370
|
+
`( fmt a1 a2 ... ) string:format out`
|
|
1371
|
+
|
|
1372
|
+
A tiny `sprintf` subset:
|
|
1373
|
+
|
|
1374
|
+
* Supports only `%s` and `%%`.
|
|
1375
|
+
* Any other specifier (`%d`, `%f`, …) causes the builtin to fail.
|
|
1376
|
+
* Missing arguments are treated as empty strings.
|
|
1377
|
+
|
|
1378
|
+
### Containment and prefix/suffix tests (constraints)
|
|
1379
|
+
|
|
1380
|
+
* `string:contains`
|
|
1381
|
+
* `string:containsIgnoringCase`
|
|
1382
|
+
* `string:startsWith`
|
|
1383
|
+
* `string:endsWith`
|
|
1384
|
+
|
|
1385
|
+
All are pure tests: they succeed or fail.
|
|
1386
|
+
|
|
1387
|
+
### Case-insensitive equality tests (constraints)
|
|
1388
|
+
|
|
1389
|
+
* `string:equalIgnoringCase`
|
|
1390
|
+
* `string:notEqualIgnoringCase`
|
|
1391
|
+
|
|
1392
|
+
### Lexicographic comparisons (constraints)
|
|
1393
|
+
|
|
1394
|
+
* `string:greaterThan`
|
|
1395
|
+
* `string:lessThan`
|
|
1396
|
+
* `string:notGreaterThan` (≤ in Unicode codepoint order)
|
|
1397
|
+
* `string:notLessThan` (≥ in Unicode codepoint order)
|
|
1398
|
+
|
|
1399
|
+
These compare JavaScript strings directly, i.e., Unicode code unit order (practically “lexicographic” for many uses, but not locale-aware collation).
|
|
1400
|
+
|
|
1401
|
+
### Regex-based tests and extraction
|
|
1402
|
+
|
|
1403
|
+
Eyeling compiles patterns using JavaScript `RegExp`, with a small compatibility layer:
|
|
1404
|
+
|
|
1405
|
+
* If the pattern uses Unicode property escapes (like `\p{L}`) or code point escapes (`\u{...}`), Eyeling enables the `/u` flag.
|
|
1406
|
+
* In Unicode mode, some “identity escapes” that would be SyntaxErrors in JS are sanitized in a conservative way.
|
|
1407
|
+
|
|
1408
|
+
#### `string:matches` / `string:notMatches` (constraints)
|
|
1409
|
+
|
|
1410
|
+
**Shape:**
|
|
1411
|
+
`data string:matches pattern`
|
|
1412
|
+
|
|
1413
|
+
Tests whether `pattern` matches `data`.
|
|
1414
|
+
|
|
1415
|
+
#### `string:replace`
|
|
1416
|
+
|
|
1417
|
+
**Shape:**
|
|
1418
|
+
`( data pattern replacement ) string:replace out`
|
|
1419
|
+
|
|
1420
|
+
* Compiles `pattern` as a global regex (`/g`).
|
|
1421
|
+
* Uses JavaScript replacement semantics (so `$1`, `$2`, etc. work).
|
|
1422
|
+
* Returns the replaced string.
|
|
1423
|
+
|
|
1424
|
+
#### `string:scrape`
|
|
1425
|
+
|
|
1426
|
+
**Shape:**
|
|
1427
|
+
`( data pattern ) string:scrape out`
|
|
1428
|
+
|
|
1429
|
+
Matches the regex once and returns the **first capturing group** (group 1). If there is no match or no group, it fails.
|
|
1430
|
+
|
|
1431
|
+
### JSON pointer lookup
|
|
1432
|
+
|
|
1433
|
+
#### `string:jsonPointer`
|
|
1434
|
+
|
|
1435
|
+
**Shape:**
|
|
1436
|
+
`( jsonText pointer ) string:jsonPointer value`
|
|
1437
|
+
|
|
1438
|
+
This builtin is intentionally “bridgey”: it lets you reach into JSON and get back an RDF/N3 term.
|
|
1439
|
+
|
|
1440
|
+
Rules:
|
|
1441
|
+
|
|
1442
|
+
* `jsonText` must be an `rdf:JSON` literal (Eyeling is permissive and may accept a couple of equivalent datatype spellings).
|
|
1443
|
+
* `pointer` is a string; Eyeling supports:
|
|
1444
|
+
|
|
1445
|
+
* standard RFC 6901 pointers like `/a/b/0`
|
|
1446
|
+
* URI fragment form like `#/a/b` (it is decoded first)
|
|
1447
|
+
* The JSON is parsed and cached; pointer results are cached per `(jsonText, pointer)`.
|
|
1448
|
+
|
|
1449
|
+
Returned terms follow Eyeling’s `jsonToTerm` mapping:
|
|
1450
|
+
|
|
1451
|
+
* JSON `null` → `"null"` (a plain string literal)
|
|
1452
|
+
* JSON string → plain string literal
|
|
1453
|
+
* JSON number → numeric token literal (untyped)
|
|
1454
|
+
* JSON boolean → `true` / `false` token literal (untyped boolean token)
|
|
1455
|
+
* JSON array → an N3 list term whose elements are recursively converted
|
|
1456
|
+
* JSON object → an `rdf:JSON` literal containing the object’s JSON text
|
|
1457
|
+
|
|
1458
|
+
This design keeps the builtin total and predictable even for nested structures.
|
|
1459
|
+
|
|
1460
|
+
## 11.4 `log:outputString` as a controlled side effect
|
|
1461
|
+
|
|
1462
|
+
From a logic-programming point of view, printing is awkward: if you print *during* proof search, you risk producing output along branches that later backtrack, or producing the same line multiple times in different derivations. Eyeling avoids that whole class of problems by treating “output” as **data**.
|
|
1463
|
+
|
|
1464
|
+
The predicate `log:outputString` is the only officially supported “side-effect channel”, and even it is handled in two phases:
|
|
1465
|
+
|
|
1466
|
+
1. **During reasoning (declarative phase):**
|
|
1467
|
+
`log:outputString` behaves like a constraint-style builtin: it succeeds when its arguments are well-formed and sufficiently bound (notably, when the object is a string literal that can be emitted). Importantly, it does *not* print anything at this time. If a rule derives a triple like:
|
|
1468
|
+
|
|
1469
|
+
```n3
|
|
1470
|
+
:k log:outputString "Hello\n".
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
then that triple simply becomes part of the fact base like any other fact.
|
|
1474
|
+
|
|
1475
|
+
2. **After reasoning (rendering phase):**
|
|
1476
|
+
Once saturation finishes, Eyeling scans the *final closure* for `log:outputString` facts and renders them deterministically. Concretely, the CLI collects all such triples, orders them in a stable way (using the subject as a key so output order is reproducible), and concatenates their string objects into the final emitted text.
|
|
1477
|
+
|
|
1478
|
+
This separation is not just an aesthetic choice; it preserves the meaning of logic search:
|
|
1479
|
+
|
|
1480
|
+
* Proof search may explore multiple branches and backtrack. Because output is only rendered from the **final** set of facts, backtracking cannot “un-print” anything and cannot cause duplicated prints from transient branches.
|
|
1481
|
+
* Output becomes explainable. If you enable proof comments or inspect the closure, `log:outputString` facts can be traced back to the rules that produced them.
|
|
1482
|
+
* Output becomes compositional. You can reason about output strings (e.g., sort them, filter them, derive them conditionally) just like any other data.
|
|
1483
|
+
|
|
1484
|
+
In short: Eyeling makes `log:outputString` safe by refusing to treat it as an immediate effect. It is a *declarative output fact* whose concrete rendering is a final, deterministic post-processing step.
|
|
1485
|
+
|
|
1486
|
+
---
|
|
1487
|
+
|
|
1488
|
+
<a id="ch12"></a>
|
|
1489
|
+
## Chapter 12 — Dereferencing and web-like semantics (`lib/deref.js`)
|
|
1490
|
+
|
|
1491
|
+
Some N3 workflows treat IRIs as pointers to more knowledge. Eyeling supports this with:
|
|
1492
|
+
|
|
1493
|
+
* `log:content` — fetch raw text
|
|
1494
|
+
* `log:semantics` — fetch and parse into a formula
|
|
1495
|
+
* `log:semanticsOrError` — produce either a formula or an error literal
|
|
1496
|
+
|
|
1497
|
+
`deref.js` is deliberately synchronous so the engine can remain synchronous.
|
|
1498
|
+
|
|
1499
|
+
### 12.1 Two environments: Node vs browser/worker
|
|
1500
|
+
|
|
1501
|
+
* In **Node**, dereferencing can read:
|
|
1502
|
+
|
|
1503
|
+
* HTTP(S) via a subprocess (still synchronous)
|
|
1504
|
+
* local files (including `file://` URIs) via `fs.readFileSync`
|
|
1505
|
+
* in practice, any non-http IRI is treated as a local path for convenience.
|
|
1506
|
+
|
|
1507
|
+
* In **browser/worker**, dereferencing uses synchronous XHR, subject to CORS, and only for HTTP(S).
|
|
1508
|
+
|
|
1509
|
+
### 12.2 Caching
|
|
1510
|
+
|
|
1511
|
+
Dereferencing is cached by IRI-without-fragment (fragments are stripped). There are separate caches for:
|
|
1512
|
+
|
|
1513
|
+
* raw content text
|
|
1514
|
+
* parsed semantics (GraphTerm)
|
|
1515
|
+
* semantics-or-error
|
|
1516
|
+
|
|
1517
|
+
This is both a performance and a stability feature: repeated `log:semantics` calls in backward proofs won’t keep refetching.
|
|
1518
|
+
|
|
1519
|
+
### 12.3 HTTPS enforcement
|
|
1520
|
+
|
|
1521
|
+
Eyeling can optionally rewrite `http://…` to `https://…` before dereferencing (CLI `--enforce-https`, or API option). This is a pragmatic “make more things work in modern environments” knob.
|
|
1522
|
+
|
|
1523
|
+
---
|
|
1524
|
+
|
|
1525
|
+
<a id="ch13"></a>
|
|
1526
|
+
## Chapter 13 — Printing, proofs, and the user-facing output
|
|
1527
|
+
|
|
1528
|
+
Once reasoning is done (or as it happens in streaming mode), Eyeling converts derived facts back to N3.
|
|
1529
|
+
|
|
1530
|
+
### 13.1 Printing terms and triples (`lib/printing.js`)
|
|
1531
|
+
|
|
1532
|
+
Printing handles:
|
|
1533
|
+
|
|
1534
|
+
* compact qnames via `PrefixEnv`
|
|
1535
|
+
* `rdf:type` as `a`
|
|
1536
|
+
* `owl:sameAs` as `=`
|
|
1537
|
+
* nice formatting for lists and formulas
|
|
1538
|
+
|
|
1539
|
+
The printer is intentionally simple; it prints what Eyeling can parse.
|
|
1540
|
+
|
|
1541
|
+
### 13.2 Proof comments: local justifications, not full proof trees
|
|
1542
|
+
|
|
1543
|
+
When enabled, Eyeling prints a compact comment block per derived triple:
|
|
1544
|
+
|
|
1545
|
+
* the derived triple
|
|
1546
|
+
* the instantiated rule body that was provable
|
|
1547
|
+
* the schematic forward rule that produced it
|
|
1548
|
+
|
|
1549
|
+
It’s a “why this triple holds” explanation, not a globally exported proof graph.
|
|
1550
|
+
|
|
1551
|
+
### 13.3 Streaming derived facts
|
|
1552
|
+
|
|
1553
|
+
The engine’s `reasonStream` API can accept an `onDerived` callback. Each time a new forward fact is derived, Eyeling can report it immediately.
|
|
1554
|
+
|
|
1555
|
+
This is especially useful in interactive demos (and is the basis of the playground streaming tab).
|
|
1556
|
+
|
|
1557
|
+
---
|
|
1558
|
+
|
|
1559
|
+
<a id="ch14"></a>
|
|
1560
|
+
## Chapter 14 — Entry points: CLI, bundle exports, and npm API
|
|
1561
|
+
|
|
1562
|
+
Eyeling exposes itself in three layers.
|
|
1563
|
+
|
|
1564
|
+
### 14.1 The bundled CLI (`eyeling.js`)
|
|
1565
|
+
|
|
1566
|
+
The bundle contains the whole engine. The CLI path is the “canonical behavior”:
|
|
1567
|
+
|
|
1568
|
+
* parse input file
|
|
1569
|
+
* reason to closure
|
|
1570
|
+
* print derived triples or output strings
|
|
1571
|
+
* optional proof comments
|
|
1572
|
+
* optional streaming
|
|
1573
|
+
|
|
1574
|
+
### 14.2 `lib/entry.js`: bundler-friendly exports
|
|
1575
|
+
|
|
1576
|
+
`lib/entry.js` exports:
|
|
1577
|
+
|
|
1578
|
+
* public APIs: `reasonStream`, `main`, `version`
|
|
1579
|
+
* plus a curated set of internals used by the demo (`lex`, `Parser`, `forwardChain`, etc.)
|
|
1580
|
+
|
|
1581
|
+
### 14.3 `index.js`: the npm API wrapper
|
|
1582
|
+
|
|
1583
|
+
The npm `reason(...)` function does something intentionally simple and robust:
|
|
1584
|
+
|
|
1585
|
+
* write your N3 input to a temp file
|
|
1586
|
+
* spawn the bundled CLI (`node eyeling.js ... input.n3`)
|
|
1587
|
+
* return stdout (and forward stderr)
|
|
1588
|
+
|
|
1589
|
+
This ensures the API matches the CLI perfectly and keeps the public surface small.
|
|
1590
|
+
|
|
1591
|
+
One practical implication:
|
|
1592
|
+
|
|
1593
|
+
* if you want *in-process* access to the engine objects (facts arrays, derived proof objects), use `reasonStream` from the bundle entry rather than the subprocess-based API.
|
|
1594
|
+
|
|
1595
|
+
---
|
|
1596
|
+
|
|
1597
|
+
<a id="ch15"></a>
|
|
1598
|
+
## Chapter 15 — A worked example: Socrates, step by step
|
|
1599
|
+
|
|
1600
|
+
Consider:
|
|
1601
|
+
|
|
1602
|
+
```n3
|
|
1603
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
|
|
1604
|
+
@prefix : <http://example.org/socrates#>.
|
|
1605
|
+
|
|
1606
|
+
:Socrates a :Human.
|
|
1607
|
+
:Human rdfs:subClassOf :Mortal.
|
|
1608
|
+
|
|
1609
|
+
{ ?S a ?A. ?A rdfs:subClassOf ?B } => { ?S a ?B }.
|
|
1610
|
+
```
|
|
1611
|
+
|
|
1612
|
+
What Eyeling does:
|
|
1613
|
+
|
|
1614
|
+
1. Parsing yields two facts:
|
|
1615
|
+
|
|
1616
|
+
* `(:Socrates rdf:type :Human)`
|
|
1617
|
+
* `(:Human rdfs:subClassOf :Mortal)`
|
|
1618
|
+
and one forward rule:
|
|
1619
|
+
* premise goals: `?S a ?A`, `?A rdfs:subClassOf ?B`
|
|
1620
|
+
* head: `?S a ?B`
|
|
1621
|
+
|
|
1622
|
+
2. Forward chaining scans the rule and calls `proveGoals` on the body.
|
|
1623
|
+
|
|
1624
|
+
3. Proving `?S a ?A` matches the first fact, producing `{ S = :Socrates, A = :Human }`.
|
|
1625
|
+
|
|
1626
|
+
4. With that substitution, the second goal becomes `:Human rdfs:subClassOf ?B`.
|
|
1627
|
+
It matches the second fact, extending to `{ B = :Mortal }`.
|
|
1628
|
+
|
|
1629
|
+
5. Eyeling instantiates the head `?S a ?B` → `:Socrates a :Mortal`.
|
|
1630
|
+
|
|
1631
|
+
6. The triple is ground and not already present, so it is added and (optionally) printed.
|
|
1632
|
+
|
|
1633
|
+
That’s the whole engine in miniature: unify, compose substitutions, emit head triples.
|
|
1634
|
+
|
|
1635
|
+
---
|
|
1636
|
+
|
|
1637
|
+
<a id="ch16"></a>
|
|
1638
|
+
## Chapter 16 — Extending Eyeling (without breaking it)
|
|
1639
|
+
|
|
1640
|
+
Eyeling is small, which makes it pleasant to extend — but there are a few invariants worth respecting.
|
|
1641
|
+
|
|
1642
|
+
### 16.1 Adding a builtin
|
|
1643
|
+
|
|
1644
|
+
Most extensions belong in `evalBuiltin`:
|
|
1645
|
+
|
|
1646
|
+
* Decide if your builtin is:
|
|
1647
|
+
|
|
1648
|
+
* a test (0/1 solution)
|
|
1649
|
+
* functional (bind output)
|
|
1650
|
+
* generator (many solutions)
|
|
1651
|
+
* Return *deltas* `{ varName: Term }`, not full substitutions.
|
|
1652
|
+
* Be cautious with fully-unbound cases: generators can explode the search space.
|
|
1653
|
+
|
|
1654
|
+
If your builtin needs a stable view of the closure, follow the scoped-builtin pattern:
|
|
1655
|
+
|
|
1656
|
+
* read from `facts.__scopedSnapshot`
|
|
1657
|
+
* honor `facts.__scopedClosureLevel` and priority gating
|
|
1658
|
+
|
|
1659
|
+
### 16.2 Adding new term shapes
|
|
1660
|
+
|
|
1661
|
+
If you add a new Term subclass, you’ll likely need to touch:
|
|
1662
|
+
|
|
1663
|
+
* printing (`termToN3`)
|
|
1664
|
+
* unification and equality (`unifyTerm`, `termsEqual`, fast keys)
|
|
1665
|
+
* variable collection for compaction (`gcCollectVarsInTerm`)
|
|
1666
|
+
* groundness checks
|
|
1667
|
+
|
|
1668
|
+
### 16.3 Parser extensions
|
|
1669
|
+
|
|
1670
|
+
If you extend parsing, preserve the Rule invariants:
|
|
1671
|
+
|
|
1672
|
+
* rule premise is a triple list
|
|
1673
|
+
* rule conclusion is a triple list
|
|
1674
|
+
* blanks in premise are lifted (or handled consistently)
|
|
1675
|
+
* `headBlankLabels` must reflect blanks occurring explicitly in the head *before* skolemization
|
|
1676
|
+
|
|
1677
|
+
---
|
|
1678
|
+
|
|
1679
|
+
<a id="epilogue"></a>
|
|
1680
|
+
## Epilogue: the philosophy of this engine
|
|
1681
|
+
|
|
1682
|
+
Eyeling’s codebase is compact because it chooses one powerful idea and leans into it:
|
|
1683
|
+
|
|
1684
|
+
> **Use backward proving as the “executor” for forward rule bodies.**
|
|
1685
|
+
|
|
1686
|
+
That design makes built-ins and backward rules feel like a standard library of relations, while forward chaining still gives you the determinism and “materialized closure” feel of Datalog.
|
|
1687
|
+
|
|
1688
|
+
If you remember only one sentence from this handbook, make it this:
|
|
1689
|
+
|
|
1690
|
+
**Eyeling is a forward-chaining engine whose rule bodies are solved by a Prolog-like backward prover with built-ins.**
|
|
1691
|
+
|
|
1692
|
+
Everything else is engineering detail — interesting, careful, sometimes subtle — but always in service of that core shape.
|
|
1693
|
+
|