eyeling 1.11.21 → 1.11.22
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 +405 -422
- package/LICENSE.md +3 -15
- package/README.md +3 -4
- package/SEMANTICS.md +10 -25
- package/examples/deep-taxonomy-10.n3 +0 -1
- package/examples/deep-taxonomy-100.n3 +0 -1
- package/examples/deep-taxonomy-1000.n3 +0 -1
- package/examples/deep-taxonomy-10000.n3 +0 -1
- package/examples/deep-taxonomy-100000.n3 +100015 -0
- package/examples/output/deep-taxonomy-100000.n3 +300004 -0
- package/eyeling.js +2 -2
- package/index.d.ts +2 -2
- package/lib/builtins.js +2 -2
- package/package.json +2 -1
- package/test/api.test.js +2 -2
- package/test/playground.test.js +1 -1
- package/tools/bundle.js +0 -0
- package/tools/n3gen.js +0 -0
package/HANDBOOK.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## A compact Notation3 reasoner in JavaScript — a handbook
|
|
4
4
|
|
|
5
|
-
> This handbook is written for a computer science student who wants to understand Eyeling as
|
|
5
|
+
> This handbook is written for a computer science student who wants to understand Eyeling as _code_ and as a _reasoning machine_.
|
|
6
6
|
> It’s meant to be read linearly, but each chapter stands on its own.
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
## Contents
|
|
10
9
|
|
|
11
10
|
- [Preface](#preface)
|
|
@@ -31,6 +30,7 @@
|
|
|
31
30
|
---
|
|
32
31
|
|
|
33
32
|
<a id="preface"></a>
|
|
33
|
+
|
|
34
34
|
## Preface: what Eyeling is (and what it is not)
|
|
35
35
|
|
|
36
36
|
Eyeling is a small Notation3 (N3) reasoner implemented in JavaScript. Its job is to take:
|
|
@@ -43,11 +43,12 @@ and compute consequences until nothing new follows.
|
|
|
43
43
|
If you’ve seen Datalog or Prolog, the shape will feel familiar. Eyeling blends both:
|
|
44
44
|
|
|
45
45
|
- **Forward chaining** (like Datalog saturation) for `=>` rules.
|
|
46
|
-
- **Backward chaining** (like Prolog goal solving) for `<=` rules
|
|
46
|
+
- **Backward chaining** (like Prolog goal solving) for `<=` rules _and_ for built-in predicates.
|
|
47
47
|
|
|
48
|
-
That last point is the heart of Eyeling’s design:
|
|
48
|
+
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.
|
|
49
49
|
|
|
50
50
|
Eyeling deliberately keeps the implementation small and dependency-free:
|
|
51
|
+
|
|
51
52
|
- the published package includes a single bundled file (`eyeling.js`)
|
|
52
53
|
- the source is organized into `lib/*` modules that read like a miniature compiler + logic engine.
|
|
53
54
|
|
|
@@ -56,6 +57,7 @@ This handbook is a tour of that miniature system.
|
|
|
56
57
|
---
|
|
57
58
|
|
|
58
59
|
<a id="ch01"></a>
|
|
60
|
+
|
|
59
61
|
## Chapter 1 — The execution model in one picture
|
|
60
62
|
|
|
61
63
|
Let’s name the pieces:
|
|
@@ -101,6 +103,7 @@ Because `PROVE` can call built-ins (math, string, list, crypto, dereferencing…
|
|
|
101
103
|
---
|
|
102
104
|
|
|
103
105
|
<a id="ch02"></a>
|
|
106
|
+
|
|
104
107
|
## Chapter 2 — The repository, as a guided reading path
|
|
105
108
|
|
|
106
109
|
If you want to follow the code in the same order Eyeling “thinks”, read:
|
|
@@ -121,7 +124,7 @@ If you want to follow the code in the same order Eyeling “thinks”, read:
|
|
|
121
124
|
6. `lib/builtins.js` — builtin predicate evaluation plus shared literal/number/string/list helpers:
|
|
122
125
|
- `makeBuiltins(deps)` dependency-injects engine hooks (unification, proving, deref, …)
|
|
123
126
|
- exports `evalBuiltin(...)` and `isBuiltinPred(...)` back to the engine
|
|
124
|
-
- includes `materializeRdfLists(...)`, a small pre-pass that rewrites
|
|
127
|
+
- includes `materializeRdfLists(...)`, a small pre-pass that rewrites _anonymous_ `rdf:first`/`rdf:rest` linked lists into concrete N3 list terms so `list:*` builtins can work uniformly
|
|
125
128
|
7. `lib/explain.js` — proof comments + `log:outputString` aggregation (fact ordering and pretty output).
|
|
126
129
|
8. `lib/deref.js` — synchronous dereferencing for `log:content` / `log:semantics` (used by builtins and engine).
|
|
127
130
|
9. `lib/printing.js` — conversion back to N3 text.
|
|
@@ -139,6 +142,7 @@ text → tokens → AST (facts + rules) → engine → derived facts → printer
|
|
|
139
142
|
---
|
|
140
143
|
|
|
141
144
|
<a id="ch03"></a>
|
|
145
|
+
|
|
142
146
|
## Chapter 3 — The data model: terms, triples, formulas, rules (`lib/prelude.js`)
|
|
143
147
|
|
|
144
148
|
Eyeling uses a small AST. You can think of it as the “instruction set” for the rest of the reasoner.
|
|
@@ -170,7 +174,7 @@ A rule is:
|
|
|
170
174
|
Two details matter later:
|
|
171
175
|
|
|
172
176
|
1. **Inference fuse**: a forward rule whose conclusion is the literal `false` acts as a hard failure. (More in Chapter 10.)
|
|
173
|
-
2. **`headBlankLabels`** records which blank node labels occur
|
|
177
|
+
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.)
|
|
174
178
|
|
|
175
179
|
### 3.3 Interning
|
|
176
180
|
|
|
@@ -182,7 +186,7 @@ Eyeling interns IRIs and Literals by string value. Interning is a quiet performa
|
|
|
182
186
|
|
|
183
187
|
In addition, interned **Iri**/**Literal** terms (and generated **Blank** terms) get a small, non-enumerable integer id `.__tid` that is stable for the lifetime of the process. This `__tid` is used as the engine’s “fast key”:
|
|
184
188
|
|
|
185
|
-
- fact indexes (`__byPred` / `__byPS` / `__byPO`) key by `__tid` values **and store fact
|
|
189
|
+
- fact indexes (`__byPred` / `__byPS` / `__byPO`) key by `__tid` values **and store fact _indices_** (predicate buckets are keyed by `predicate.__tid`, and PS/PO buckets are keyed by the subject/object `.__tid`; buckets contain integer indices into the `facts` array)
|
|
186
190
|
- duplicate detection uses `"sid pid oid"` where each component is a `__tid`
|
|
187
191
|
- unification/equality has an early-out when two terms share the same `__tid`
|
|
188
192
|
|
|
@@ -201,6 +205,7 @@ Terms are treated as immutable: once interned/created, the code assumes you won
|
|
|
201
205
|
---
|
|
202
206
|
|
|
203
207
|
<a id="ch04"></a>
|
|
208
|
+
|
|
204
209
|
## Chapter 4 — From characters to AST: lexing and parsing (`lib/lexer.js`, `lib/parser.js`)
|
|
205
210
|
|
|
206
211
|
Eyeling’s parser is intentionally pragmatic: it aims to accept “the stuff people actually write” in N3/Turtle, including common shorthand.
|
|
@@ -265,19 +270,20 @@ true => { :Program :loaded true }.
|
|
|
265
270
|
|
|
266
271
|
Internally:
|
|
267
272
|
|
|
268
|
-
|
|
269
|
-
|
|
273
|
+
- `true` becomes “empty triple list”
|
|
274
|
+
- `false` becomes “no head triples” _plus_ the `isFuse` flag if forward.
|
|
270
275
|
|
|
271
276
|
---
|
|
272
277
|
|
|
273
278
|
<a id="ch05"></a>
|
|
279
|
+
|
|
274
280
|
## Chapter 5 — Rule normalization: “compile-time” semantics (`lib/rules.js`)
|
|
275
281
|
|
|
276
282
|
Before rules hit the engine, Eyeling performs two lightweight transformations.
|
|
277
283
|
|
|
278
284
|
### 5.1 Lifting blank nodes in rule bodies into variables
|
|
279
285
|
|
|
280
|
-
In N3 practice, blanks in
|
|
286
|
+
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.
|
|
281
287
|
|
|
282
288
|
So a premise like:
|
|
283
289
|
|
|
@@ -293,17 +299,17 @@ acts like:
|
|
|
293
299
|
|
|
294
300
|
This avoids the “existential in the body” trap and matches how most rule authors expect N3 to behave.
|
|
295
301
|
|
|
296
|
-
Blanks in the **conclusion** are
|
|
302
|
+
Blanks in the **conclusion** are _not_ lifted — they remain blanks and later become existentials (Chapter 9).
|
|
297
303
|
|
|
298
304
|
### 5.2 Delaying constraints
|
|
299
305
|
|
|
300
306
|
Some built-ins don’t generate bindings; they only test conditions:
|
|
301
307
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
- `math:greaterThan`, `math:lessThan`, `math:equalTo`, …
|
|
309
|
+
- `string:matches`, `string:contains`, …
|
|
310
|
+
- `log:notIncludes`, `log:forAllIn`, `log:outputString`, …
|
|
305
311
|
|
|
306
|
-
Eyeling treats these as “constraints” and moves them to the
|
|
312
|
+
Eyeling treats these as “constraints” and moves them to the _end_ of a forward rule premise. This is a Prolog-style heuristic:
|
|
307
313
|
|
|
308
314
|
> Bind variables first; only then run pure checks.
|
|
309
315
|
|
|
@@ -320,29 +326,30 @@ _:d rdf:first :b.
|
|
|
320
326
|
_:d rdf:rest rdf:nil.
|
|
321
327
|
```
|
|
322
328
|
|
|
323
|
-
Eyeling supports
|
|
329
|
+
Eyeling supports _both_ representations:
|
|
324
330
|
|
|
325
|
-
|
|
326
|
-
|
|
331
|
+
- **Concrete N3 lists** like `(:a :b)` are parsed as `ListTerm([...])` directly.
|
|
332
|
+
- **RDF collections** using `rdf:first`/`rdf:rest` can be traversed by list-aware builtins.
|
|
327
333
|
|
|
328
334
|
To make list handling simpler and faster, Eyeling runs a small pre-pass called `materializeRdfLists(...)` (implemented in `lib/builtins.js` and invoked by the CLI/entry code). It:
|
|
329
335
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
336
|
+
- scans the **input triples** for well‑formed `rdf:first`/`rdf:rest` chains,
|
|
337
|
+
- **rewrites only anonymous (blank-node) list nodes** into concrete `ListTerm(...)`,
|
|
338
|
+
- and applies that rewrite consistently across the input triple set and all rule premises/heads.
|
|
333
339
|
|
|
334
340
|
Why only blank nodes? Named list nodes (IRIs) must keep their identity, because some programs treat them as addressable resources; Eyeling leaves those as `rdf:first`/`rdf:rest` graphs so list builtins can still walk them when needed.
|
|
335
341
|
|
|
336
342
|
---
|
|
337
343
|
|
|
338
344
|
<a id="ch06"></a>
|
|
345
|
+
|
|
339
346
|
## Chapter 6 — Equality, alpha-equivalence, and unification (`lib/engine.js`)
|
|
340
347
|
|
|
341
348
|
Once you enter `engine.js`, you enter the “physics layer.” Everything else depends on the correctness of:
|
|
342
349
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
350
|
+
- equality and normalization (especially for literals)
|
|
351
|
+
- alpha-equivalence for formulas
|
|
352
|
+
- unification and substitution application
|
|
346
353
|
|
|
347
354
|
### 6.1 Two equalities: structural vs alpha-equivalent
|
|
348
355
|
|
|
@@ -352,18 +359,18 @@ But **quoted formulas** (`GraphTerm`) demand something stronger. Two formulas sh
|
|
|
352
359
|
|
|
353
360
|
That’s alpha-equivalence:
|
|
354
361
|
|
|
355
|
-
|
|
362
|
+
- `{ _:x :p ?y. }` should match `{ _:z :p ?w. }`
|
|
356
363
|
|
|
357
364
|
Eyeling implements alpha-equivalence by checking whether there exists a consistent renaming mapping between the two formulas’ variables/blanks that makes the triples match.
|
|
358
365
|
|
|
359
366
|
### 6.2 Groundness: “variables inside formulas don’t leak”
|
|
360
367
|
|
|
361
|
-
Eyeling makes a deliberate choice about
|
|
368
|
+
Eyeling makes a deliberate choice about _groundness_:
|
|
362
369
|
|
|
363
|
-
|
|
364
|
-
|
|
370
|
+
- a triple is “ground” if it has no free variables in normal positions
|
|
371
|
+
- **variables inside a `GraphTerm` do not make the surrounding triple non-ground**
|
|
365
372
|
|
|
366
|
-
This is encoded in functions like `isGroundTermInGraph`. It’s what makes it possible to assert and store triples that
|
|
373
|
+
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.
|
|
367
374
|
|
|
368
375
|
### 6.3 Substitutions: chaining and application
|
|
369
376
|
|
|
@@ -375,29 +382,29 @@ A substitution is a plain JS object:
|
|
|
375
382
|
|
|
376
383
|
When applying substitutions, Eyeling follows **chains**:
|
|
377
384
|
|
|
378
|
-
|
|
385
|
+
- if `X → Var(Y)` and `Y → Iri(...)`, applying to `X` yields the IRI.
|
|
379
386
|
|
|
380
387
|
Chains arise naturally during unification (e.g. when variables unify with other variables) and during rule firing.
|
|
381
388
|
|
|
382
|
-
At the API boundary, a substitution is still just a plain object, and unification still produces
|
|
389
|
+
At the API boundary, a substitution is still just a plain object, and unification still produces _delta_ objects (small `{ varName: Term }` maps).
|
|
383
390
|
But inside the hot backward-chaining loop (`proveGoals`), Eyeling uses a Prolog-style **trail** to avoid cloning substitutions at every step:
|
|
384
391
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
392
|
+
- keep one **mutable** substitution object during DFS
|
|
393
|
+
- when a candidate match yields a delta, **apply the bindings in place**
|
|
394
|
+
- record newly-bound variable names on a **trail stack**
|
|
395
|
+
- on backtracking, **undo** only the bindings pushed since a saved “mark”
|
|
389
396
|
|
|
390
397
|
This keeps the search semantics identical, but removes the “copy a growing object per step” cost that dominates deep/branchy proofs. Returned solutions are emitted as compact plain objects, so callers never observe mutation.
|
|
391
398
|
|
|
392
399
|
Implementation details (and why they matter):
|
|
393
400
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
+
- **`applySubstTerm` is the only “chain chaser”.** It follows `Var → Term` links until it reaches a stable term.
|
|
402
|
+
- Unification’s occurs-check prevents most cycles, but `applySubstTerm` still defends against accidental cyclic chains.
|
|
403
|
+
- The cycle guard is written to avoid allocating a `Set` in the common case (short chains).
|
|
404
|
+
- **Structural sharing is deliberate.** Applying a substitution often changes nothing:
|
|
405
|
+
- `applySubstTerm` returns the original term when it is unaffected.
|
|
406
|
+
- list/open-list/graph terms are only rebuilt if at least one component changes (lazy copy-on-change).
|
|
407
|
+
- `applySubstTriple` returns the original `Triple` when `s/p/o` are unchanged.
|
|
401
408
|
|
|
402
409
|
These “no-op returns” are one of the biggest practical performance wins in the engine: backward chaining and forward rule instantiation apply substitutions constantly, so avoiding allocations reduces GC pressure without changing semantics.
|
|
403
410
|
|
|
@@ -405,32 +412,32 @@ These “no-op returns” are one of the biggest practical performance wins in t
|
|
|
405
412
|
|
|
406
413
|
Unification is implemented in `unifyTerm` / `unifyTriple`, with support for:
|
|
407
414
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
* otherwise: backtracking order-insensitive matching while threading the substitution
|
|
415
|
+
- variable binding with occurs check
|
|
416
|
+
- list unification (elementwise)
|
|
417
|
+
- open-list unification (prefix + tail variable)
|
|
418
|
+
- formula unification via graph unification:
|
|
419
|
+
- fast path: identical triple list
|
|
420
|
+
- otherwise: backtracking order-insensitive matching while threading the substitution
|
|
415
421
|
|
|
416
422
|
There are two key traits of Eyeling’s graph unification:
|
|
417
423
|
|
|
418
|
-
1. It’s
|
|
419
|
-
2. It’s
|
|
424
|
+
1. It’s _set-like_: order doesn’t matter.
|
|
425
|
+
2. It’s _substitution-threaded_: choices made while matching one triple restrict the remaining matches, just like Prolog.
|
|
420
426
|
|
|
421
427
|
### 6.5 Literals: lexical vs semantic equality
|
|
422
428
|
|
|
423
429
|
Eyeling keeps literal values as raw strings, but it parses and normalizes where needed:
|
|
424
430
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
431
|
+
- `literalParts(lit)` splits lexical form and datatype IRI
|
|
432
|
+
- it recognizes RDF JSON datatype (`rdf:JSON` / `<...rdf#JSON>`)
|
|
433
|
+
- it includes caches for numeric parsing, integer parsing (`BigInt`), and numeric metadata.
|
|
428
434
|
|
|
429
435
|
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).
|
|
430
436
|
|
|
431
437
|
---
|
|
432
438
|
|
|
433
439
|
<a id="ch07"></a>
|
|
440
|
+
|
|
434
441
|
## Chapter 7 — Facts as a database: indexing and fast duplicate checks
|
|
435
442
|
|
|
436
443
|
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.
|
|
@@ -441,10 +448,10 @@ Facts live in an array `facts: Triple[]`.
|
|
|
441
448
|
|
|
442
449
|
Eyeling attaches hidden (non-enumerable) index fields:
|
|
443
450
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
451
|
+
- `facts.__byPred: Map<predicateId, number[]>` where each entry is an index into `facts` (and `predicateId` is `predicate.__tid`)
|
|
452
|
+
- `facts.__byPS: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
|
|
453
|
+
- `facts.__byPO: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
|
|
454
|
+
- `facts.__keySet: Set<string>` for a fast-path `"sid pid oid"` key (all three are `__tid` values)
|
|
448
455
|
|
|
449
456
|
`termFastKey(term)` returns a `termId` (`term.__tid`) for **Iri**, **Literal**, and **Blank** terms, and `null` for structured terms (lists, quoted graphs) and variables.
|
|
450
457
|
|
|
@@ -467,11 +474,12 @@ When adding derived facts, Eyeling uses a fast-path duplicate check when possibl
|
|
|
467
474
|
- If all three terms have a fast key (Iri/Literal/Blank → `__tid`), it checks membership in `facts.__keySet` using the `"sid pid oid"` key.
|
|
468
475
|
- Otherwise (lists, quoted graphs, variables), it falls back to structural triple equality.
|
|
469
476
|
|
|
470
|
-
This still treats blanks correctly: blanks are
|
|
477
|
+
This still treats blanks correctly: blanks are _not_ interchangeable; the blank **label** (and thus its `__tid`) is part of the key.
|
|
471
478
|
|
|
472
479
|
---
|
|
473
480
|
|
|
474
481
|
<a id="ch08"></a>
|
|
482
|
+
|
|
475
483
|
## Chapter 8 — Backward chaining: the proof engine (`proveGoals`)
|
|
476
484
|
|
|
477
485
|
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.
|
|
@@ -480,10 +488,10 @@ Eyeling’s backward prover is an iterative depth-first search (DFS) that looks
|
|
|
480
488
|
|
|
481
489
|
A proof state contains:
|
|
482
490
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
491
|
+
- `goals`: remaining goal triples
|
|
492
|
+
- `subst`: current substitution
|
|
493
|
+
- `depth`: current depth (used for compaction heuristics)
|
|
494
|
+
- `visited`: previously-seen goals (loop prevention)
|
|
487
495
|
|
|
488
496
|
### 8.2 The proving loop
|
|
489
497
|
|
|
@@ -491,18 +499,16 @@ At each step:
|
|
|
491
499
|
|
|
492
500
|
1. If no goals remain: emit the current substitution as a solution.
|
|
493
501
|
2. Otherwise:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
* attempt to satisfy it in three ways:
|
|
498
|
-
|
|
502
|
+
- take the first goal
|
|
503
|
+
- apply the current substitution to it
|
|
504
|
+
- attempt to satisfy it in three ways:
|
|
499
505
|
1. built-ins
|
|
500
506
|
2. facts
|
|
501
507
|
3. backward rules
|
|
502
508
|
|
|
503
509
|
Eyeling’s order is intentional: built-ins often bind variables cheaply; rules expand search trees.
|
|
504
510
|
|
|
505
|
-
### 8.3 Built-ins: return
|
|
511
|
+
### 8.3 Built-ins: return _deltas_, not full substitutions
|
|
506
512
|
|
|
507
513
|
A built-in is evaluated by the engine via the builtin library in `lib/builtins.js`:
|
|
508
514
|
|
|
@@ -519,17 +525,16 @@ for delta in deltas:
|
|
|
519
525
|
|
|
520
526
|
**Implementation note (performance):** as of this version, Eyeling also avoids allocating short-lived substitution objects when matching goals against **facts** and when unifying a **backward-rule head** with the current goal. Instead of calling the pure `unifyTriple(..., subst)` (which clones the substitution on each variable bind), the prover performs an **in-place unification** directly into the mutable `substMut` store and records only the newly-bound variable names on the trail. This typically reduces GC pressure significantly on reachability / path-search workloads, where unification is executed extremely frequently.
|
|
521
527
|
|
|
522
|
-
|
|
523
528
|
So built-ins behave like relations that can generate zero, one, or many possible bindings. A list generator might yield many deltas; a numeric test yields zero or one.
|
|
524
529
|
|
|
525
530
|
#### 8.3.1 Builtin deferral and “vacuous” solutions
|
|
526
531
|
|
|
527
|
-
Conjunction in N3 is order-insensitive, but many builtins are only useful once some variables are bound by
|
|
532
|
+
Conjunction in N3 is order-insensitive, but many builtins are only useful once some variables are bound by _other_ goals in the same body. When `proveGoals` is called from forward chaining, Eyeling enables **builtin deferral**: if a builtin goal can’t make progress yet, it is rotated to the end of the goal list and retried later (with a small cycle guard to avoid infinite rotation).
|
|
528
533
|
|
|
529
534
|
“Can’t make progress” includes both cases:
|
|
530
535
|
|
|
531
536
|
- the builtin returns **no solutions** (`[]`), and
|
|
532
|
-
- the builtin returns only **vacuous solutions** (`[{}]`, i.e., success with
|
|
537
|
+
- the builtin returns only **vacuous solutions** (`[{}]`, i.e., success with _no new bindings_) while the goal still contains unbound vars/blanks.
|
|
533
538
|
|
|
534
539
|
That second case matters for “satisfiable but non-enumerating” builtins (e.g., some `log:` helpers) where early vacuous success would otherwise prevent later goals from ever binding the variables the builtin needs.
|
|
535
540
|
|
|
@@ -541,8 +546,8 @@ Eyeling prevents obvious infinite recursion by skipping a goal if it is already
|
|
|
541
546
|
|
|
542
547
|
Backward rules are indexed in `backRules.__byHeadPred`. When proving a goal with IRI predicate `p`, Eyeling retrieves:
|
|
543
548
|
|
|
544
|
-
|
|
545
|
-
|
|
549
|
+
- `rules whose head predicate is p`
|
|
550
|
+
- plus `__wildHeadPred` for rules whose head predicate is not an IRI (rare, but supported)
|
|
546
551
|
|
|
547
552
|
For each candidate rule:
|
|
548
553
|
|
|
@@ -553,7 +558,7 @@ For each candidate rule:
|
|
|
553
558
|
That “standardize apart” step is essential. Without it, reusing a rule multiple times would accidentally share variables across invocations, producing incorrect bindings.
|
|
554
559
|
|
|
555
560
|
**Implementation note (performance):** `standardizeRule` is called for every backward-rule candidate during proof search.
|
|
556
|
-
To reduce allocation pressure, Eyeling reuses a single fresh `Var(...)` object per
|
|
561
|
+
To reduce allocation pressure, Eyeling reuses a single fresh `Var(...)` object per _original_ variable name within one standardization pass (all occurrences of `?x` in the rule become the same fresh `?x__N` object). This is semantics-preserving — it still “separates” invocations — but it avoids creating many duplicate Var objects when a variable appears repeatedly in a rule body.
|
|
557
562
|
|
|
558
563
|
### 8.6 Substitution size on deep proofs
|
|
559
564
|
|
|
@@ -565,6 +570,7 @@ Eyeling currently keeps the full trail as-is during search and when emitting ans
|
|
|
565
570
|
---
|
|
566
571
|
|
|
567
572
|
<a id="ch09"></a>
|
|
573
|
+
|
|
568
574
|
## Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules (`forwardChain`)
|
|
569
575
|
|
|
570
576
|
Forward chaining is Eyeling’s outer control loop. It is where facts get added and the closure grows.
|
|
@@ -594,12 +600,12 @@ until not changed
|
|
|
594
600
|
|
|
595
601
|
There is a nice micro-compiler optimization in `runFixpoint()`:
|
|
596
602
|
|
|
597
|
-
If a rule’s head is
|
|
603
|
+
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.
|
|
598
604
|
|
|
599
605
|
In that case:
|
|
600
606
|
|
|
601
|
-
|
|
602
|
-
|
|
607
|
+
- Eyeling only needs **one** proof of the body.
|
|
608
|
+
- And if all head triples are already known, it can skip proving the body entirely.
|
|
603
609
|
|
|
604
610
|
This is a surprisingly effective optimization for “axiom-like” rules with constant heads.
|
|
605
611
|
|
|
@@ -609,9 +615,9 @@ Blank nodes in the **rule head** represent existentials: “there exists somethi
|
|
|
609
615
|
|
|
610
616
|
Eyeling handles this by replacing head blank labels with fresh blank labels of the form:
|
|
611
617
|
|
|
612
|
-
|
|
618
|
+
- `_:sk_0`, `_:sk_1`, …
|
|
613
619
|
|
|
614
|
-
But it does something subtle and important: it caches skolemization per (rule firing, head blank label), so that the
|
|
620
|
+
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.
|
|
615
621
|
|
|
616
622
|
The “firing instance” is keyed by a deterministic string derived from the instantiated body (“firingKey”). This stabilizes the closure and prevents “existential churn.”
|
|
617
623
|
|
|
@@ -623,17 +629,17 @@ Implementation: deterministic Skolem IDs live in `lib/skolem.js`; the per-firing
|
|
|
623
629
|
|
|
624
630
|
A rule whose conclusion is `false` is treated as a hard failure. During forward chaining:
|
|
625
631
|
|
|
626
|
-
|
|
627
|
-
|
|
632
|
+
- Eyeling proves the premise (it only needs one solution)
|
|
633
|
+
- if the premise is provable, it prints a message and exits with status code 2
|
|
628
634
|
|
|
629
635
|
This is Eyeling’s way to express constraints and detect inconsistencies.
|
|
630
636
|
|
|
631
637
|
### 9.5 Rule-producing rules (meta-rules)
|
|
632
638
|
|
|
633
|
-
Eyeling treats certain derived triples as
|
|
639
|
+
Eyeling treats certain derived triples as _new rules_:
|
|
634
640
|
|
|
635
|
-
|
|
636
|
-
|
|
641
|
+
- `log:implies` and `log:impliedBy` where subject/object are formulas
|
|
642
|
+
- it also accepts the literal `true` as an empty formula `{}` on either side
|
|
637
643
|
|
|
638
644
|
So these are “rule triples”:
|
|
639
645
|
|
|
@@ -646,7 +652,7 @@ true log:implies { ... }.
|
|
|
646
652
|
When such a triple is derived in a forward rule head:
|
|
647
653
|
|
|
648
654
|
1. Eyeling adds it as a fact (so you can inspect it), and
|
|
649
|
-
2. it
|
|
655
|
+
2. it _promotes_ it into a live rule by constructing a new `Rule` object and inserting it into the forward or backward rule list.
|
|
650
656
|
|
|
651
657
|
This is meta-programming: your rules can generate new rules during reasoning.
|
|
652
658
|
|
|
@@ -656,30 +662,30 @@ To keep promotion cheap, Eyeling maintains a `Set` of canonical rule keys for bo
|
|
|
656
662
|
---
|
|
657
663
|
|
|
658
664
|
<a id="ch10"></a>
|
|
665
|
+
|
|
659
666
|
## Chapter 10 — Scoped closure, priorities, and `log:conclusion`
|
|
660
667
|
|
|
661
|
-
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
|
|
668
|
+
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_.
|
|
662
669
|
|
|
663
670
|
Eyeling addresses this with a disciplined two-phase strategy and an optional priority mechanism.
|
|
664
671
|
|
|
665
672
|
### 10.1 The two-phase outer loop (Phase A / Phase B)
|
|
666
673
|
|
|
667
|
-
Forward chaining runs inside an
|
|
674
|
+
Forward chaining runs inside an _outer loop_ that alternates:
|
|
668
675
|
|
|
669
|
-
|
|
676
|
+
- **Phase A**: scoped built-ins are disabled (they “delay” by failing)
|
|
670
677
|
|
|
671
|
-
|
|
678
|
+
- Eyeling saturates normally to a fixpoint
|
|
672
679
|
|
|
673
|
-
|
|
680
|
+
- then Eyeling freezes a snapshot of the saturated facts
|
|
674
681
|
|
|
675
|
-
|
|
682
|
+
- **Phase B**: scoped built-ins are enabled, but they query only the frozen snapshot
|
|
676
683
|
|
|
677
|
-
|
|
684
|
+
- Eyeling runs saturation again (new facts can appear due to scoped queries)
|
|
678
685
|
|
|
679
686
|
This produces deterministic behavior for scoped operations: they observe a stable snapshot, not a moving target.
|
|
680
687
|
|
|
681
|
-
**Implementation note (performance):** the two-phase scheme is only needed when the program actually uses scoped built-ins.
|
|
682
|
-
If no rule contains `log:collectAllIn`, `log:forAllIn`, `log:includes`, or `log:notIncludes`, Eyeling now **skips Phase B entirely** and runs only a single saturation. This avoids re-running the forward fixpoint and can prevent a “query-like” forward rule (one whose body contains an expensive backward proof search) from being executed twice.
|
|
688
|
+
**Implementation note (performance):** the two-phase scheme is only needed when the program actually uses scoped built-ins. If no rule contains `log:collectAllIn`, `log:forAllIn`, `log:includes`, or `log:notIncludes`, Eyeling now **skips Phase B entirely** and runs only a single saturation. This avoids re-running the forward fixpoint and can prevent a “query-like” forward rule (one whose body contains an expensive backward proof search) from being executed twice.
|
|
683
689
|
|
|
684
690
|
**Implementation note (performance):** in Phase A there is no snapshot, so scoped built-ins (and priority-gated scoped queries) are guaranteed to “delay” by failing.
|
|
685
691
|
Instead of proving the entire forward-rule body only to fail at the end, Eyeling precomputes whether a forward rule depends on scoped built-ins and skips it until a snapshot exists and the requested closure level is reached. This can avoid very expensive proof searches in programs that combine recursion with `log:*In` built-ins.
|
|
@@ -688,13 +694,13 @@ Instead of proving the entire forward-rule body only to fail at the end, Eyeling
|
|
|
688
694
|
|
|
689
695
|
Eyeling introduces a `scopedClosureLevel` counter:
|
|
690
696
|
|
|
691
|
-
|
|
692
|
-
|
|
697
|
+
- level 0 means “no snapshot available” (Phase A)
|
|
698
|
+
- level 1, 2, … correspond to snapshots produced after each Phase A saturation
|
|
693
699
|
|
|
694
700
|
Some built-ins interpret a positive integer literal as a requested priority:
|
|
695
701
|
|
|
696
|
-
|
|
697
|
-
|
|
702
|
+
- `log:collectAllIn` and `log:forAllIn` use the **object position** for priority
|
|
703
|
+
- `log:includes` and `log:notIncludes` use the **subject position** for priority
|
|
698
704
|
|
|
699
705
|
If a rule requests priority `N`, Eyeling delays that builtin until `scopedClosureLevel >= N`.
|
|
700
706
|
|
|
@@ -704,12 +710,12 @@ In practice this allows rule authors to write “don’t run this scoped query u
|
|
|
704
710
|
|
|
705
711
|
`log:conclusion` is handled in a particularly elegant way:
|
|
706
712
|
|
|
707
|
-
|
|
708
|
-
|
|
713
|
+
- given a formula `{ ... }` (a `GraphTerm`),
|
|
714
|
+
- Eyeling computes the deductive closure _inside that formula_:
|
|
715
|
+
- extract rule triples inside it (`log:implies`, `log:impliedBy`)
|
|
716
|
+
- run `forwardChain` locally over those triples
|
|
709
717
|
|
|
710
|
-
|
|
711
|
-
* run `forwardChain` locally over those triples
|
|
712
|
-
* cache the result in a `WeakMap` so the same formula doesn’t get recomputed
|
|
718
|
+
- cache the result in a `WeakMap` so the same formula doesn’t get recomputed
|
|
713
719
|
|
|
714
720
|
Notably, `log:impliedBy` inside the formula is treated as forward implication too for closure computation (and also indexed as backward to help proving).
|
|
715
721
|
|
|
@@ -718,6 +724,7 @@ This makes formulas a little world you can reason about as data.
|
|
|
718
724
|
---
|
|
719
725
|
|
|
720
726
|
<a id="ch11"></a>
|
|
727
|
+
|
|
721
728
|
## Chapter 11 — Built-ins as a standard library (`lib/builtins.js`)
|
|
722
729
|
|
|
723
730
|
Built-ins are where Eyeling stops being “just a Datalog engine” and becomes a practical N3 tool.
|
|
@@ -728,49 +735,49 @@ Implementation note: builtin code lives in `lib/builtins.js` and is wired into t
|
|
|
728
735
|
|
|
729
736
|
A predicate is treated as builtin if:
|
|
730
737
|
|
|
731
|
-
|
|
738
|
+
- it is an IRI in one of the builtin namespaces:
|
|
739
|
+
- `crypto:`, `math:`, `log:`, `string:`, `time:`, `list:`
|
|
732
740
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
* unless **super restricted mode** is enabled, in which case only `log:implies` and `log:impliedBy` are treated as builtins.
|
|
741
|
+
- or it is `rdf:first` / `rdf:rest` (treated as list-like builtins)
|
|
742
|
+
- unless **super restricted mode** is enabled, in which case only `log:implies` and `log:impliedBy` are treated as builtins.
|
|
736
743
|
|
|
737
744
|
Super restricted mode exists to let you treat all other predicates as ordinary facts/rules without any built-in evaluation.
|
|
738
745
|
|
|
739
746
|
### 11.2 Built-ins return multiple solutions
|
|
740
747
|
|
|
741
|
-
Every builtin returns a list of substitution
|
|
748
|
+
Every builtin returns a list of substitution _deltas_.
|
|
742
749
|
|
|
743
750
|
That means built-ins can be:
|
|
744
751
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
752
|
+
- **functional** (return one delta binding an output)
|
|
753
|
+
- **tests** (return either `[{}]` for success or `[]` for failure)
|
|
754
|
+
- **generators** (return many deltas)
|
|
748
755
|
|
|
749
756
|
List operations are a common source of generators; numeric comparisons are tests.
|
|
750
757
|
|
|
751
|
-
Below is a drop-in replacement for **§11.3 “A tour of builtin families”** that aims to be
|
|
758
|
+
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/builtins.js`** (including the `rdf:first` / `rdf:rest` aliases).
|
|
752
759
|
|
|
753
760
|
---
|
|
754
761
|
|
|
755
762
|
## 11.3 A tour of builtin families
|
|
756
763
|
|
|
757
|
-
Eyeling’s builtins are best thought of as
|
|
764
|
+
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.
|
|
758
765
|
|
|
759
|
-
That one sentence explains a lot of “why does it behave like
|
|
766
|
+
That one sentence explains a lot of “why does it behave like _that_?”:
|
|
760
767
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
768
|
+
- Builtins are evaluated **during backward proof** (goal solving), just like facts and backward rules.
|
|
769
|
+
- A builtin may produce **zero solutions** (fail), **one solution** (deterministic succeed), or **many solutions** (a generator).
|
|
770
|
+
- Most builtins behave like relations, not like functions: they can sometimes run “backwards” (bind the subject from the object) if the implementation supports it.
|
|
771
|
+
- 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.
|
|
765
772
|
|
|
766
773
|
### 11.3.0 Reading builtin “signatures” in this handbook
|
|
767
774
|
|
|
768
775
|
The N3 Builtins tradition often describes builtins using “schema” annotations like:
|
|
769
776
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
777
|
+
- `$s+` / `$o+` — input must be bound (or at least not a variable in practice)
|
|
778
|
+
- `$s-` / `$o-` — output position (often a variable that will be bound)
|
|
779
|
+
- `$s?` / `$o?` — may be unbound
|
|
780
|
+
- `$s.i` — list element _i_ inside the subject list
|
|
774
781
|
|
|
775
782
|
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:
|
|
776
783
|
|
|
@@ -788,13 +795,12 @@ These builtins hash a string and return a lowercase hex digest as a plain string
|
|
|
788
795
|
|
|
789
796
|
### `crypto:sha`, `crypto:md5`, `crypto:sha256`, `crypto:sha512`
|
|
790
797
|
|
|
791
|
-
**Shape:**
|
|
792
|
-
`$literal crypto:sha256 $digest`
|
|
798
|
+
**Shape:** `$literal crypto:sha256 $digest`
|
|
793
799
|
|
|
794
800
|
**Semantics (Eyeling):**
|
|
795
801
|
|
|
796
|
-
|
|
797
|
-
|
|
802
|
+
- The **subject must be a literal**. Eyeling takes the literal’s lexical form (stripping quotes) as UTF-8 input.
|
|
803
|
+
- The **object** is unified with a **plain string literal** containing the hex digest.
|
|
798
804
|
|
|
799
805
|
**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).
|
|
800
806
|
|
|
@@ -821,12 +827,12 @@ A key design choice: Eyeling parses numeric terms fairly strictly, but compariso
|
|
|
821
827
|
|
|
822
828
|
These builtins succeed or fail; they do not introduce new bindings.
|
|
823
829
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
+
- `math:greaterThan` (>)
|
|
831
|
+
- `math:lessThan` (<)
|
|
832
|
+
- `math:notGreaterThan` (≤)
|
|
833
|
+
- `math:notLessThan` (≥)
|
|
834
|
+
- `math:equalTo` (=)
|
|
835
|
+
- `math:notEqualTo` (≠)
|
|
830
836
|
|
|
831
837
|
**Shapes:**
|
|
832
838
|
|
|
@@ -843,15 +849,15 @@ Eyeling also accepts an older cwm-ish variant where the **subject is a 2-element
|
|
|
843
849
|
|
|
844
850
|
**Accepted term types (Eyeling):**
|
|
845
851
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
852
|
+
- Proper XSD numeric literals (`xsd:integer`, `xsd:decimal`, `xsd:float`, `xsd:double`, and integer-derived types).
|
|
853
|
+
- Untyped numeric tokens (`123`, `-4.5`, `1.2e3`) when they look numeric.
|
|
854
|
+
- `xsd:duration` literals (treated as seconds via a simplified model).
|
|
855
|
+
- `xsd:date` and `xsd:dateTime` literals (converted to epoch seconds for comparison).
|
|
850
856
|
|
|
851
857
|
**Edge cases:**
|
|
852
858
|
|
|
853
|
-
|
|
854
|
-
|
|
859
|
+
- `NaN` is treated as **not equal to anything**, including itself, for `math:equalTo`.
|
|
860
|
+
- Comparisons involving non-parsable values simply fail.
|
|
855
861
|
|
|
856
862
|
These are pure tests. In forward rules, if a test builtin is encountered before its inputs are bound and it fails, Eyeling may **defer** it and try other goals first; once variables become bound, the test is retried.
|
|
857
863
|
|
|
@@ -865,15 +871,15 @@ These are “function-like” relations where the subject is usually a list and
|
|
|
865
871
|
|
|
866
872
|
**Shape:** `( $x1 $x2 ... ) math:sum $total`
|
|
867
873
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
874
|
+
- Subject must be a list of **at least two** numeric terms.
|
|
875
|
+
- Computes the numeric sum.
|
|
876
|
+
- 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.
|
|
871
877
|
|
|
872
878
|
#### `math:product`
|
|
873
879
|
|
|
874
880
|
**Shape:** `( $x1 $x2 ... ) math:product $total`
|
|
875
881
|
|
|
876
|
-
|
|
882
|
+
- Same conventions as `math:sum`, but multiplies.
|
|
877
883
|
|
|
878
884
|
#### `math:difference`
|
|
879
885
|
|
|
@@ -885,11 +891,10 @@ Eyeling supports:
|
|
|
885
891
|
|
|
886
892
|
1. **Numeric subtraction**: `c = a - b`.
|
|
887
893
|
2. **DateTime difference**: `(dateTime1 dateTime2) math:difference duration`
|
|
894
|
+
- Produces an `xsd:duration` in whole days (internally computed via seconds then formatted).
|
|
888
895
|
|
|
889
|
-
* Produces an `xsd:duration` in whole days (internally computed via seconds then formatted).
|
|
890
896
|
3. **DateTime minus duration**: `(dateTime duration) math:difference dateTime`
|
|
891
|
-
|
|
892
|
-
* Subtracts a duration from a dateTime and yields a new dateTime.
|
|
897
|
+
- Subtracts a duration from a dateTime and yields a new dateTime.
|
|
893
898
|
|
|
894
899
|
If the types don’t fit any supported case, the builtin fails.
|
|
895
900
|
|
|
@@ -897,33 +902,33 @@ If the types don’t fit any supported case, the builtin fails.
|
|
|
897
902
|
|
|
898
903
|
**Shape:** `( $a $b ) math:quotient $q`
|
|
899
904
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
905
|
+
- Parses both inputs as numbers.
|
|
906
|
+
- Requires finite values and `b != 0`.
|
|
907
|
+
- Computes `a / b`, picking a suitable numeric datatype for output.
|
|
903
908
|
|
|
904
909
|
#### `math:integerQuotient`
|
|
905
910
|
|
|
906
911
|
**Shape:** `( $a $b ) math:integerQuotient $q`
|
|
907
912
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
913
|
+
- Intended for integer division with remainder discarded (truncation toward zero).
|
|
914
|
+
- Prefers exact arithmetic using **BigInt** if both inputs are integer literals.
|
|
915
|
+
- Falls back to Number parsing if needed, but still requires integer-like values.
|
|
911
916
|
|
|
912
917
|
#### `math:remainder`
|
|
913
918
|
|
|
914
919
|
**Shape:** `( $a $b ) math:remainder $r`
|
|
915
920
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
921
|
+
- Integer-only modulus.
|
|
922
|
+
- Uses BigInt when possible; otherwise requires both numbers to still represent integers.
|
|
923
|
+
- Fails on division by zero.
|
|
919
924
|
|
|
920
925
|
#### `math:rounded`
|
|
921
926
|
|
|
922
927
|
**Shape:** `$x math:rounded $n`
|
|
923
928
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
929
|
+
- Rounds to nearest integer.
|
|
930
|
+
- Tie-breaking follows JavaScript `Math.round`, i.e. halves go toward **+∞** (`-1.5 -> -1`, `1.5 -> 2`).
|
|
931
|
+
- Eyeling emits the integer as an **integer token literal** (and also accepts typed numerics if they compare equal).
|
|
927
932
|
|
|
928
933
|
---
|
|
929
934
|
|
|
@@ -933,13 +938,11 @@ If the types don’t fit any supported case, the builtin fails.
|
|
|
933
938
|
|
|
934
939
|
**Shape:** `( $base $exp ) math:exponentiation $result`
|
|
935
940
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
* exponent is unbound
|
|
942
|
-
In that case it uses logarithms: `exp = log(result) / log(base)`.
|
|
941
|
+
- Forward direction: if base and exponent are numeric, computes `base ** exp`.
|
|
942
|
+
- Reverse direction (limited): Eyeling can sometimes solve for the exponent if:
|
|
943
|
+
- base and result are numeric, finite, and **positive**
|
|
944
|
+
- base is not 1
|
|
945
|
+
- exponent is unbound In that case it uses logarithms: `exp = log(result) / log(base)`.
|
|
943
946
|
|
|
944
947
|
This is a pragmatic inversion, not a full algebra system.
|
|
945
948
|
|
|
@@ -947,12 +950,12 @@ This is a pragmatic inversion, not a full algebra system.
|
|
|
947
950
|
|
|
948
951
|
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).
|
|
949
952
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
953
|
+
- `math:absoluteValue`
|
|
954
|
+
- `math:negation`
|
|
955
|
+
- `math:degrees` (and implicitly its inverse “radians” conversion)
|
|
956
|
+
- `math:sin`, `math:cos`, `math:tan`
|
|
957
|
+
- `math:asin`, `math:acos`, `math:atan`
|
|
958
|
+
- `math:sinh`, `math:cosh`, `math:tanh` (only if JS provides the functions)
|
|
956
959
|
|
|
957
960
|
**Example:**
|
|
958
961
|
|
|
@@ -973,38 +976,35 @@ Implementation: these helpers live in `lib/time.js` and are called from `lib/eng
|
|
|
973
976
|
|
|
974
977
|
### Component extractors
|
|
975
978
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
979
|
+
- `time:year`
|
|
980
|
+
- `time:month`
|
|
981
|
+
- `time:day`
|
|
982
|
+
- `time:hour`
|
|
983
|
+
- `time:minute`
|
|
984
|
+
- `time:second`
|
|
982
985
|
|
|
983
|
-
**Shape:**
|
|
984
|
-
`$dt time:month $m`
|
|
986
|
+
**Shape:** `$dt time:month $m`
|
|
985
987
|
|
|
986
988
|
**Semantics:**
|
|
987
989
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
990
|
+
- Subject must be an `xsd:dateTime` literal in a format Eyeling can parse.
|
|
991
|
+
- Object becomes the corresponding integer component (as an integer token literal).
|
|
992
|
+
- If the object is already a numeric literal, Eyeling accepts it if it matches.
|
|
991
993
|
|
|
992
994
|
### `time:timeZone`
|
|
993
995
|
|
|
994
|
-
**Shape:**
|
|
995
|
-
`$dt time:timeZone $tz`
|
|
996
|
+
**Shape:** `$dt time:timeZone $tz`
|
|
996
997
|
|
|
997
998
|
Returns the trailing zone designator:
|
|
998
999
|
|
|
999
|
-
|
|
1000
|
-
|
|
1000
|
+
- `"Z"` for UTC, or
|
|
1001
|
+
- a string like `"+02:00"` / `"-05:00"`
|
|
1001
1002
|
|
|
1002
1003
|
It yields a **plain string literal** (and also accepts typed `xsd:string` literals).
|
|
1003
1004
|
|
|
1004
1005
|
### `time:localTime`
|
|
1005
1006
|
|
|
1006
|
-
**Shape:**
|
|
1007
|
-
`"" time:localTime ?now`
|
|
1007
|
+
**Shape:** `"" time:localTime ?now`
|
|
1008
1008
|
|
|
1009
1009
|
Binds `?now` to the current local time as an `xsd:dateTime` literal.
|
|
1010
1010
|
|
|
@@ -1021,49 +1021,46 @@ Eyeling has a real internal list term (`ListTerm`) that corresponds to N3’s `(
|
|
|
1021
1021
|
|
|
1022
1022
|
### RDF collections (`rdf:first` / `rdf:rest`) are materialized
|
|
1023
1023
|
|
|
1024
|
-
N3 and RDF can also express lists as linked blank nodes using `rdf:first` / `rdf:rest` and `rdf:nil`. Eyeling
|
|
1024
|
+
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.
|
|
1025
1025
|
|
|
1026
1026
|
For convenience and compatibility, Eyeling treats:
|
|
1027
1027
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1028
|
+
- `rdf:first` as an alias of `list:first`
|
|
1029
|
+
- `rdf:rest` as an alias of `list:rest`
|
|
1030
1030
|
|
|
1031
1031
|
### Core list destructuring
|
|
1032
1032
|
|
|
1033
1033
|
#### `list:first` (and `rdf:first`)
|
|
1034
1034
|
|
|
1035
|
-
**Shape:**
|
|
1036
|
-
`(a b c) list:first a`
|
|
1035
|
+
**Shape:** `(a b c) list:first a`
|
|
1037
1036
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1037
|
+
- Succeeds iff the subject is a **non-empty closed list**.
|
|
1038
|
+
- Unifies the object with the first element.
|
|
1040
1039
|
|
|
1041
1040
|
#### `list:rest` (and `rdf:rest`)
|
|
1042
1041
|
|
|
1043
|
-
**Shape:**
|
|
1044
|
-
`(a b c) list:rest (b c)`
|
|
1042
|
+
**Shape:** `(a b c) list:rest (b c)`
|
|
1045
1043
|
|
|
1046
1044
|
Eyeling supports both:
|
|
1047
1045
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1046
|
+
- closed lists `(a b c)`, and
|
|
1047
|
+
- _open lists_ of the form `(a b ... ?T)` internally.
|
|
1050
1048
|
|
|
1051
1049
|
For open lists, “rest” preserves openness:
|
|
1052
1050
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1051
|
+
- Rest of `(a ... ?T)` is `?T`
|
|
1052
|
+
- Rest of `(a b ... ?T)` is `(b ... ?T)`
|
|
1055
1053
|
|
|
1056
1054
|
#### `list:firstRest`
|
|
1057
1055
|
|
|
1058
1056
|
This is a very useful “paired” view of a list.
|
|
1059
1057
|
|
|
1060
|
-
**Forward shape:**
|
|
1061
|
-
`(a b c) list:firstRest (a (b c))`
|
|
1058
|
+
**Forward shape:** `(a b c) list:firstRest (a (b c))`
|
|
1062
1059
|
|
|
1063
1060
|
**Backward shapes (construction):**
|
|
1064
1061
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1062
|
+
- If the object is `(first restList)`, it can construct the list.
|
|
1063
|
+
- If `rest` is a variable, Eyeling constructs an open list term.
|
|
1067
1064
|
|
|
1068
1065
|
This is the closest thing to Prolog’s `[H|T]` in Eyeling.
|
|
1069
1066
|
|
|
@@ -1077,26 +1074,23 @@ These builtins can yield multiple solutions.
|
|
|
1077
1074
|
|
|
1078
1075
|
#### `list:member`
|
|
1079
1076
|
|
|
1080
|
-
**Shape:**
|
|
1081
|
-
`(a b c) list:member ?x`
|
|
1077
|
+
**Shape:** `(a b c) list:member ?x`
|
|
1082
1078
|
|
|
1083
1079
|
Generates one solution per element, unifying the object with each member.
|
|
1084
1080
|
|
|
1085
1081
|
#### `list:in`
|
|
1086
1082
|
|
|
1087
|
-
**Shape:**
|
|
1088
|
-
`?x list:in (a b c)`
|
|
1083
|
+
**Shape:** `?x list:in (a b c)`
|
|
1089
1084
|
|
|
1090
1085
|
Same idea, but the list is in the **object** position and the **subject** is unified with each element.
|
|
1091
1086
|
|
|
1092
1087
|
#### `list:iterate`
|
|
1093
1088
|
|
|
1094
|
-
**Shape:**
|
|
1095
|
-
`(a b c) list:iterate ?pair`
|
|
1089
|
+
**Shape:** `(a b c) list:iterate ?pair`
|
|
1096
1090
|
|
|
1097
1091
|
Generates `(index value)` pairs with **0-based indices**:
|
|
1098
1092
|
|
|
1099
|
-
|
|
1093
|
+
- `(0 a)`, `(1 b)`, `(2 c)`, …
|
|
1100
1094
|
|
|
1101
1095
|
A nice ergonomic detail: the object may be a pattern such as:
|
|
1102
1096
|
|
|
@@ -1108,16 +1102,15 @@ In that case Eyeling unifies `?i` with `1` and checks the value part appropriate
|
|
|
1108
1102
|
|
|
1109
1103
|
#### `list:memberAt`
|
|
1110
1104
|
|
|
1111
|
-
**Shape:**
|
|
1112
|
-
`( (a b c) 1 ) list:memberAt b`
|
|
1105
|
+
**Shape:** `( (a b c) 1 ) list:memberAt b`
|
|
1113
1106
|
|
|
1114
1107
|
The subject must be a 2-element list: `(listTerm indexTerm)`.
|
|
1115
1108
|
|
|
1116
1109
|
Eyeling can use this relationally:
|
|
1117
1110
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1111
|
+
- If the index is bound, it can return the value.
|
|
1112
|
+
- If the value is bound, it can search for indices that match.
|
|
1113
|
+
- If both are variables, it generates pairs (similar to `iterate`, but with separate index/value logic).
|
|
1121
1114
|
|
|
1122
1115
|
Indices are **0-based**.
|
|
1123
1116
|
|
|
@@ -1127,8 +1120,7 @@ Indices are **0-based**.
|
|
|
1127
1120
|
|
|
1128
1121
|
#### `list:length`
|
|
1129
1122
|
|
|
1130
|
-
**Shape:**
|
|
1131
|
-
`(a b c) list:length 3`
|
|
1123
|
+
**Shape:** `(a b c) list:length 3`
|
|
1132
1124
|
|
|
1133
1125
|
Returns the length as an integer token literal.
|
|
1134
1126
|
|
|
@@ -1136,8 +1128,7 @@ A small but intentional strictness: if the object is already ground, Eyeling doe
|
|
|
1136
1128
|
|
|
1137
1129
|
#### `list:last`
|
|
1138
1130
|
|
|
1139
|
-
**Shape:**
|
|
1140
|
-
`(a b c) list:last c`
|
|
1131
|
+
**Shape:** `(a b c) list:last c`
|
|
1141
1132
|
|
|
1142
1133
|
Returns the last element of a non-empty list.
|
|
1143
1134
|
|
|
@@ -1145,15 +1136,14 @@ Returns the last element of a non-empty list.
|
|
|
1145
1136
|
|
|
1146
1137
|
Reversible in the sense that either side may be the list:
|
|
1147
1138
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1139
|
+
- If subject is a list, object becomes its reversal.
|
|
1140
|
+
- If object is a list, subject becomes its reversal.
|
|
1150
1141
|
|
|
1151
1142
|
It does not enumerate arbitrary reversals; it’s a deterministic transform once one side is known.
|
|
1152
1143
|
|
|
1153
1144
|
#### `list:remove`
|
|
1154
1145
|
|
|
1155
|
-
**Shape:**
|
|
1156
|
-
`( (a b a c) a ) list:remove (b c)`
|
|
1146
|
+
**Shape:** `( (a b a c) a ) list:remove (b c)`
|
|
1157
1147
|
|
|
1158
1148
|
Removes all occurrences of an item from a list.
|
|
1159
1149
|
|
|
@@ -1161,8 +1151,7 @@ Important constraint: the item to remove must be **ground** (fully known) before
|
|
|
1161
1151
|
|
|
1162
1152
|
#### `list:notMember` (constraint)
|
|
1163
1153
|
|
|
1164
|
-
**Shape:**
|
|
1165
|
-
`(a b c) list:notMember x`
|
|
1154
|
+
**Shape:** `(a b c) list:notMember x`
|
|
1166
1155
|
|
|
1167
1156
|
Succeeds iff the object cannot be unified with any element of the subject list. As a test, it typically works best once its inputs are bound; in forward rules Eyeling may defer it if it is reached before bindings are available.
|
|
1168
1157
|
|
|
@@ -1170,23 +1159,21 @@ Succeeds iff the object cannot be unified with any element of the subject list.
|
|
|
1170
1159
|
|
|
1171
1160
|
This is list concatenation, but Eyeling implements it in a pleasantly relational way.
|
|
1172
1161
|
|
|
1173
|
-
**Forward shape:**
|
|
1174
|
-
`( (a b) (c) (d e) ) list:append (a b c d e)`
|
|
1162
|
+
**Forward shape:** `( (a b) (c) (d e) ) list:append (a b c d e)`
|
|
1175
1163
|
|
|
1176
1164
|
Subject is a list of lists; object is their concatenation.
|
|
1177
1165
|
|
|
1178
|
-
**Splitting (reverse-ish) mode:**
|
|
1179
|
-
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.
|
|
1166
|
+
**Splitting (reverse-ish) mode:** 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.
|
|
1180
1167
|
|
|
1181
1168
|
#### `list:sort`
|
|
1182
1169
|
|
|
1183
1170
|
Sorts a list into a deterministic order.
|
|
1184
1171
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1172
|
+
- Requires the input list’s elements to be **ground**.
|
|
1173
|
+
- Orders literals numerically when both sides look numeric; otherwise compares their lexical strings.
|
|
1174
|
+
- Orders lists lexicographically by elements.
|
|
1175
|
+
- Orders IRIs by IRI string.
|
|
1176
|
+
- Falls back to a stable structural key for mixed cases.
|
|
1190
1177
|
|
|
1191
1178
|
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.
|
|
1192
1179
|
|
|
@@ -1194,8 +1181,7 @@ Like `reverse`, this is “reversible” only in the sense that if one side is a
|
|
|
1194
1181
|
|
|
1195
1182
|
This is one of Eyeling’s most powerful list builtins because it calls back into the reasoner.
|
|
1196
1183
|
|
|
1197
|
-
**Shape:**
|
|
1198
|
-
`( (x1 x2 x3) ex:pred ) list:map ?outList`
|
|
1184
|
+
**Shape:** `( (x1 x2 x3) ex:pred ) list:map ?outList`
|
|
1199
1185
|
|
|
1200
1186
|
Semantics:
|
|
1201
1187
|
|
|
@@ -1207,7 +1193,8 @@ Semantics:
|
|
|
1207
1193
|
el predicateIri ?y.
|
|
1208
1194
|
```
|
|
1209
1195
|
|
|
1210
|
-
using
|
|
1196
|
+
using _the full engine_ (facts, backward rules, and builtins).
|
|
1197
|
+
|
|
1211
1198
|
4. All resulting `?y` values are collected in proof order and concatenated into the output list.
|
|
1212
1199
|
5. If an element produces no solutions, it contributes nothing.
|
|
1213
1200
|
|
|
@@ -1217,14 +1204,13 @@ This makes `list:map` a compact “query over a list” operator.
|
|
|
1217
1204
|
|
|
1218
1205
|
## 11.3.5 `log:` — unification, formulas, scoping, and meta-level control
|
|
1219
1206
|
|
|
1220
|
-
The `log:` family is where N3 stops being “RDF with rules” and becomes a
|
|
1207
|
+
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.
|
|
1221
1208
|
|
|
1222
1209
|
### Equality and inequality
|
|
1223
1210
|
|
|
1224
1211
|
#### `log:equalTo`
|
|
1225
1212
|
|
|
1226
|
-
**Shape:**
|
|
1227
|
-
`$x log:equalTo $y`
|
|
1213
|
+
**Shape:** `$x log:equalTo $y`
|
|
1228
1214
|
|
|
1229
1215
|
This is simply **term unification**: it succeeds if the two terms can be unified and returns any bindings that result.
|
|
1230
1216
|
|
|
@@ -1238,26 +1224,24 @@ In Eyeling, a quoted formula `{ ... }` is represented as a `GraphTerm` whose con
|
|
|
1238
1224
|
|
|
1239
1225
|
#### `log:conjunction`
|
|
1240
1226
|
|
|
1241
|
-
**Shape:**
|
|
1242
|
-
`( F1 F2 ... ) log:conjunction F`
|
|
1227
|
+
**Shape:** `( F1 F2 ... ) log:conjunction F`
|
|
1243
1228
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1229
|
+
- Subject is a list of formulas.
|
|
1230
|
+
- Object becomes a formula containing all triples from all inputs.
|
|
1231
|
+
- Duplicate triples are removed.
|
|
1232
|
+
- The literal `true` is treated as the **empty formula** and is ignored in the merge.
|
|
1248
1233
|
|
|
1249
1234
|
#### `log:conclusion`
|
|
1250
1235
|
|
|
1251
|
-
**Shape:**
|
|
1252
|
-
`F log:conclusion C`
|
|
1236
|
+
**Shape:** `F log:conclusion C`
|
|
1253
1237
|
|
|
1254
|
-
Computes the
|
|
1238
|
+
Computes the _deductive closure_ of the formula `F` **using only the information inside `F`**:
|
|
1255
1239
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1240
|
+
- Eyeling starts with all triples inside `F` as facts.
|
|
1241
|
+
- It treats `{A} => {B}` (represented internally as a `log:implies` triple between formulas) as a forward rule.
|
|
1242
|
+
- It treats `{A} <= {B}` as the corresponding forward direction for closure purposes.
|
|
1243
|
+
- Then it forward-chains to a fixpoint _within that local fact set_.
|
|
1244
|
+
- The result is returned as a formula containing all derived triples.
|
|
1261
1245
|
|
|
1262
1246
|
Eyeling caches `log:conclusion` results per formula object, so repeated calls with the same formula term are cheap.
|
|
1263
1247
|
|
|
@@ -1267,35 +1251,32 @@ These builtins reach outside the current fact set. They are synchronous by desig
|
|
|
1267
1251
|
|
|
1268
1252
|
#### `log:content`
|
|
1269
1253
|
|
|
1270
|
-
**Shape:**
|
|
1271
|
-
`<doc> log:content ?txt`
|
|
1254
|
+
**Shape:** `<doc> log:content ?txt`
|
|
1272
1255
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1256
|
+
- Dereferences the IRI (fragment stripped) and returns the raw bytes as an `xsd:string` literal.
|
|
1257
|
+
- In Node: HTTP(S) is fetched synchronously; non-HTTP is treated as a local file path (including `file://`).
|
|
1258
|
+
- In browsers/workers: uses synchronous XHR (subject to CORS).
|
|
1276
1259
|
|
|
1277
1260
|
#### `log:semantics`
|
|
1278
1261
|
|
|
1279
|
-
**Shape:**
|
|
1280
|
-
`<doc> log:semantics ?formula`
|
|
1262
|
+
**Shape:** `<doc> log:semantics ?formula`
|
|
1281
1263
|
|
|
1282
1264
|
Dereferences and parses the remote/local resource as N3/Turtle-like syntax, returning a formula.
|
|
1283
1265
|
|
|
1284
|
-
A nice detail: top-level rules in the parsed document are represented
|
|
1266
|
+
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.
|
|
1285
1267
|
|
|
1286
1268
|
#### `log:semanticsOrError`
|
|
1287
1269
|
|
|
1288
1270
|
Like `log:semantics`, but on failure it returns a string literal such as:
|
|
1289
1271
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1272
|
+
- `error(dereference_failed,...)`
|
|
1273
|
+
- `error(parse_error,...)`
|
|
1292
1274
|
|
|
1293
1275
|
This is convenient in robust pipelines where you want logic that can react to failures.
|
|
1294
1276
|
|
|
1295
1277
|
#### `log:parsedAsN3`
|
|
1296
1278
|
|
|
1297
|
-
**Shape:**
|
|
1298
|
-
`" ...n3 text... " log:parsedAsN3 ?formula`
|
|
1279
|
+
**Shape:** `" ...n3 text... " log:parsedAsN3 ?formula`
|
|
1299
1280
|
|
|
1300
1281
|
Parses an in-memory string as N3 and returns the corresponding formula.
|
|
1301
1282
|
|
|
@@ -1305,10 +1286,10 @@ Parses an in-memory string as N3 and returns the corresponding formula.
|
|
|
1305
1286
|
|
|
1306
1287
|
Returns one of four IRIs:
|
|
1307
1288
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1289
|
+
- `log:Formula` (quoted graph)
|
|
1290
|
+
- `log:Literal`
|
|
1291
|
+
- `rdf:List` (closed or open list terms)
|
|
1292
|
+
- `log:Other` (IRIs, blank nodes, etc.)
|
|
1312
1293
|
|
|
1313
1294
|
### Literal constructors
|
|
1314
1295
|
|
|
@@ -1318,9 +1299,9 @@ These two are classic N3 “bridge” operators between structured data and conc
|
|
|
1318
1299
|
|
|
1319
1300
|
Relates a datatype literal to a pair `(lex datatypeIri)`.
|
|
1320
1301
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1302
|
+
- If object is a literal, it can produce the subject list `(stringLiteral datatypeIri)`.
|
|
1303
|
+
- If subject is such a list, it can produce the corresponding datatype literal.
|
|
1304
|
+
- If both subject and object are variables, Eyeling treats this as satisfiable and succeeds once.
|
|
1324
1305
|
|
|
1325
1306
|
Language-tagged strings are normalized: they are treated as having datatype `rdf:langString`.
|
|
1326
1307
|
|
|
@@ -1328,20 +1309,20 @@ Language-tagged strings are normalized: they are treated as having datatype `rdf
|
|
|
1328
1309
|
|
|
1329
1310
|
Relates a language-tagged literal to a pair `(lex langTag)`.
|
|
1330
1311
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1312
|
+
- If object is `"hello"@en`, subject can become `("hello" "en")`.
|
|
1313
|
+
- If subject is `("hello" "en")`, object can become `"hello"@en`.
|
|
1314
|
+
- Fully unbound succeeds once.
|
|
1334
1315
|
|
|
1335
1316
|
### Rules as data: introspection
|
|
1336
1317
|
|
|
1337
1318
|
#### `log:implies` and `log:impliedBy`
|
|
1338
1319
|
|
|
1339
|
-
As
|
|
1320
|
+
As _syntax_, Eyeling parses `{A} => {B}` and `{A} <= {B}` into internal forward/backward rules.
|
|
1340
1321
|
|
|
1341
|
-
As
|
|
1322
|
+
As _builtins_, `log:implies` and `log:impliedBy` let you **inspect the currently loaded rule set**:
|
|
1342
1323
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1324
|
+
- `log:implies` enumerates forward rules as `(premiseFormula, conclusionFormula)` pairs.
|
|
1325
|
+
- `log:impliedBy` enumerates backward rules similarly.
|
|
1345
1326
|
|
|
1346
1327
|
Each enumerated rule is standardized apart (fresh variable names) before unification so you can safely query over it.
|
|
1347
1328
|
|
|
@@ -1349,29 +1330,26 @@ Each enumerated rule is standardized apart (fresh variable names) before unifica
|
|
|
1349
1330
|
|
|
1350
1331
|
#### `log:includes`
|
|
1351
1332
|
|
|
1352
|
-
**Shape:**
|
|
1353
|
-
`Scope log:includes GoalFormula`
|
|
1333
|
+
**Shape:** `Scope log:includes GoalFormula`
|
|
1354
1334
|
|
|
1355
1335
|
This proves all triples in `GoalFormula` as goals, returning the substitutions that make them provable.
|
|
1356
1336
|
|
|
1357
1337
|
Eyeling has **two modes**:
|
|
1358
1338
|
|
|
1359
1339
|
1. **Explicit scope graph**: if `Scope` is a formula `{...}`
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
* External rules are not used.
|
|
1340
|
+
- Eyeling reasons _only inside that formula_ (its triples are the fact store).
|
|
1341
|
+
- External rules are not used.
|
|
1363
1342
|
|
|
1364
1343
|
2. **Priority-gated global scope**: otherwise
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
* If the closure level is below `N`, the builtin “delays” by failing at that point in the search.
|
|
1344
|
+
- Eyeling uses a _frozen snapshot_ of the current global closure.
|
|
1345
|
+
- The “priority” is read from the subject if it’s a positive integer literal `N`.
|
|
1346
|
+
- If the closure level is below `N`, the builtin “delays” by failing at that point in the search.
|
|
1369
1347
|
|
|
1370
1348
|
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.
|
|
1371
1349
|
|
|
1372
1350
|
Also supported:
|
|
1373
1351
|
|
|
1374
|
-
|
|
1352
|
+
- The object may be the literal `true`, meaning the empty formula, which is always included (subject to the priority gating above).
|
|
1375
1353
|
|
|
1376
1354
|
#### `log:notIncludes` (constraint)
|
|
1377
1355
|
|
|
@@ -1379,20 +1357,18 @@ Negation-as-failure version: it succeeds iff `log:includes` would yield no solut
|
|
|
1379
1357
|
|
|
1380
1358
|
#### `log:collectAllIn`
|
|
1381
1359
|
|
|
1382
|
-
**Shape:**
|
|
1383
|
-
`( ValueTemplate WhereFormula OutList ) log:collectAllIn Scope`
|
|
1360
|
+
**Shape:** `( ValueTemplate WhereFormula OutList ) log:collectAllIn Scope`
|
|
1384
1361
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1362
|
+
- Proves `WhereFormula` in the chosen scope.
|
|
1363
|
+
- For each solution, applies it to `ValueTemplate` and collects the instantiated terms into a list.
|
|
1364
|
+
- Unifies `OutList` with that list.
|
|
1365
|
+
- If `OutList` is a blank node, Eyeling just checks satisfiable without binding/collecting.
|
|
1389
1366
|
|
|
1390
1367
|
This is essentially a list-producing “findall”.
|
|
1391
1368
|
|
|
1392
1369
|
#### `log:forAllIn` (constraint)
|
|
1393
1370
|
|
|
1394
|
-
**Shape:**
|
|
1395
|
-
`( WhereFormula ThenFormula ) log:forAllIn Scope`
|
|
1371
|
+
**Shape:** `( WhereFormula ThenFormula ) log:forAllIn Scope`
|
|
1396
1372
|
|
|
1397
1373
|
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.
|
|
1398
1374
|
|
|
@@ -1402,18 +1378,17 @@ As a pure test (no returned bindings), this typically works best once its inputs
|
|
|
1402
1378
|
|
|
1403
1379
|
#### `log:skolem`
|
|
1404
1380
|
|
|
1405
|
-
**Shape:**
|
|
1406
|
-
`$groundTerm log:skolem ?iri`
|
|
1381
|
+
**Shape:** `$groundTerm log:skolem ?iri`
|
|
1407
1382
|
|
|
1408
|
-
Deterministically maps a
|
|
1383
|
+
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.
|
|
1409
1384
|
|
|
1410
1385
|
#### `log:uri`
|
|
1411
1386
|
|
|
1412
1387
|
Bidirectional conversion between IRIs and their string form:
|
|
1413
1388
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1389
|
+
- If subject is an IRI, object can be unified with a string literal of its IRI.
|
|
1390
|
+
- 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.
|
|
1391
|
+
- Some “fully unbound / don’t-care” combinations succeed once to avoid infinite enumeration.
|
|
1417
1392
|
|
|
1418
1393
|
### Side effects and output directives
|
|
1419
1394
|
|
|
@@ -1433,8 +1408,8 @@ Implementation: this is implemented by `lib/trace.js` and called from `lib/engin
|
|
|
1433
1408
|
|
|
1434
1409
|
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:
|
|
1435
1410
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1411
|
+
- When you run Eyeling with `--strings` / `-r`, the CLI collects all `log:outputString` triples from the _saturated_ closure.
|
|
1412
|
+
- It sorts them deterministically by the subject “key” and concatenates the string values in that order.
|
|
1438
1413
|
|
|
1439
1414
|
This is a pure test/side-effect marker (it shouldn’t drive search; it should merely validate that strings exist once other reasoning has produced them). In forward rules Eyeling may defer it if it is reached before the terms are usable.
|
|
1440
1415
|
|
|
@@ -1444,53 +1419,51 @@ This is a pure test/side-effect marker (it shouldn’t drive search; it should m
|
|
|
1444
1419
|
|
|
1445
1420
|
Eyeling implements string builtins with a deliberate interpretation of “domain is `xsd:string`”:
|
|
1446
1421
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1422
|
+
- Any **IRI** can be cast to a string (its IRI text).
|
|
1423
|
+
- Any **literal** can be cast to a string:
|
|
1424
|
+
- quoted lexical forms decode N3/Turtle escapes,
|
|
1425
|
+
- unquoted lexical tokens are taken as-is (numbers, booleans, dateTimes, …).
|
|
1449
1426
|
|
|
1450
|
-
|
|
1451
|
-
* unquoted lexical tokens are taken as-is (numbers, booleans, dateTimes, …).
|
|
1452
|
-
* Blank nodes, lists, formulas, and variables are not string-castable (and cause the builtin to fail).
|
|
1427
|
+
- Blank nodes, lists, formulas, and variables are not string-castable (and cause the builtin to fail).
|
|
1453
1428
|
|
|
1454
1429
|
### Construction and concatenation
|
|
1455
1430
|
|
|
1456
1431
|
#### `string:concatenation`
|
|
1457
1432
|
|
|
1458
|
-
**Shape:**
|
|
1459
|
-
`( s1 s2 ... ) string:concatenation s`
|
|
1433
|
+
**Shape:** `( s1 s2 ... ) string:concatenation s`
|
|
1460
1434
|
|
|
1461
1435
|
Casts each element to a string and concatenates.
|
|
1462
1436
|
|
|
1463
1437
|
#### `string:format`
|
|
1464
1438
|
|
|
1465
|
-
**Shape:**
|
|
1466
|
-
`( fmt a1 a2 ... ) string:format out`
|
|
1439
|
+
**Shape:** `( fmt a1 a2 ... ) string:format out`
|
|
1467
1440
|
|
|
1468
1441
|
A tiny `sprintf` subset:
|
|
1469
1442
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1443
|
+
- Supports only `%s` and `%%`.
|
|
1444
|
+
- Any other specifier (`%d`, `%f`, …) causes the builtin to fail.
|
|
1445
|
+
- Missing arguments are treated as empty strings.
|
|
1473
1446
|
|
|
1474
1447
|
### Containment and prefix/suffix tests (constraints)
|
|
1475
1448
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1449
|
+
- `string:contains`
|
|
1450
|
+
- `string:containsIgnoringCase`
|
|
1451
|
+
- `string:startsWith`
|
|
1452
|
+
- `string:endsWith`
|
|
1480
1453
|
|
|
1481
1454
|
All are pure tests: they succeed or fail.
|
|
1482
1455
|
|
|
1483
1456
|
### Case-insensitive equality tests (constraints)
|
|
1484
1457
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1458
|
+
- `string:equalIgnoringCase`
|
|
1459
|
+
- `string:notEqualIgnoringCase`
|
|
1487
1460
|
|
|
1488
1461
|
### Lexicographic comparisons (constraints)
|
|
1489
1462
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1463
|
+
- `string:greaterThan`
|
|
1464
|
+
- `string:lessThan`
|
|
1465
|
+
- `string:notGreaterThan` (≤ in Unicode codepoint order)
|
|
1466
|
+
- `string:notLessThan` (≥ in Unicode codepoint order)
|
|
1494
1467
|
|
|
1495
1468
|
These compare JavaScript strings directly, i.e., Unicode code unit order (practically “lexicographic” for many uses, but not locale-aware collation).
|
|
1496
1469
|
|
|
@@ -1498,41 +1471,37 @@ These compare JavaScript strings directly, i.e., Unicode code unit order (practi
|
|
|
1498
1471
|
|
|
1499
1472
|
Eyeling compiles patterns using JavaScript `RegExp`, with a small compatibility layer:
|
|
1500
1473
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1474
|
+
- If the pattern uses Unicode property escapes (like `\p{L}`) or code point escapes (`\u{...}`), Eyeling enables the `/u` flag.
|
|
1475
|
+
- In Unicode mode, some “identity escapes” that would be SyntaxErrors in JS are sanitized in a conservative way.
|
|
1503
1476
|
|
|
1504
1477
|
#### `string:matches` / `string:notMatches` (constraints)
|
|
1505
1478
|
|
|
1506
|
-
**Shape:**
|
|
1507
|
-
`data string:matches pattern`
|
|
1479
|
+
**Shape:** `data string:matches pattern`
|
|
1508
1480
|
|
|
1509
1481
|
Tests whether `pattern` matches `data`.
|
|
1510
1482
|
|
|
1511
1483
|
#### `string:replace`
|
|
1512
1484
|
|
|
1513
|
-
**Shape:**
|
|
1514
|
-
`( data pattern replacement ) string:replace out`
|
|
1485
|
+
**Shape:** `( data pattern replacement ) string:replace out`
|
|
1515
1486
|
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1487
|
+
- Compiles `pattern` as a global regex (`/g`).
|
|
1488
|
+
- Uses JavaScript replacement semantics (so `$1`, `$2`, etc. work).
|
|
1489
|
+
- Returns the replaced string.
|
|
1519
1490
|
|
|
1520
1491
|
#### `string:scrape`
|
|
1521
1492
|
|
|
1522
|
-
**Shape:**
|
|
1523
|
-
`( data pattern ) string:scrape out`
|
|
1493
|
+
**Shape:** `( data pattern ) string:scrape out`
|
|
1524
1494
|
|
|
1525
1495
|
Matches the regex once and returns the **first capturing group** (group 1). If there is no match or no group, it fails.
|
|
1526
1496
|
|
|
1527
|
-
|
|
1528
1497
|
## 11.4 `log:outputString` as a controlled side effect
|
|
1529
1498
|
|
|
1530
|
-
From a logic-programming point of view, printing is awkward: if you print
|
|
1499
|
+
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**.
|
|
1531
1500
|
|
|
1532
1501
|
The predicate `log:outputString` is the only officially supported “side-effect channel”, and even it is handled in two phases:
|
|
1533
1502
|
|
|
1534
1503
|
1. **During reasoning (declarative phase):**
|
|
1535
|
-
`log:outputString` behaves like a constraint-style builtin (implemented in `lib/builtins.js`): 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
|
|
1504
|
+
`log:outputString` behaves like a constraint-style builtin (implemented in `lib/builtins.js`): 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:
|
|
1536
1505
|
|
|
1537
1506
|
```n3
|
|
1538
1507
|
:k log:outputString "Hello\n".
|
|
@@ -1540,47 +1509,47 @@ The predicate `log:outputString` is the only officially supported “side-effect
|
|
|
1540
1509
|
|
|
1541
1510
|
then that triple simply becomes part of the fact base like any other fact.
|
|
1542
1511
|
|
|
1543
|
-
2. **After reasoning (rendering phase):**
|
|
1544
|
-
Once saturation finishes, Eyeling scans the *final closure* for `log:outputString` facts and renders them deterministically (this post-pass lives in `lib/explain.js`). 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.
|
|
1512
|
+
2. **After reasoning (rendering phase):** Once saturation finishes, Eyeling scans the _final closure_ for `log:outputString` facts and renders them deterministically (this post-pass lives in `lib/explain.js`). 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.
|
|
1545
1513
|
|
|
1546
1514
|
This separation is not just an aesthetic choice; it preserves the meaning of logic search:
|
|
1547
1515
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1516
|
+
- 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.
|
|
1517
|
+
- 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.
|
|
1518
|
+
- Output becomes compositional. You can reason about output strings (e.g., sort them, filter them, derive them conditionally) just like any other data.
|
|
1551
1519
|
|
|
1552
|
-
In short: Eyeling makes `log:outputString` safe by refusing to treat it as an immediate effect. It is a
|
|
1520
|
+
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.
|
|
1553
1521
|
|
|
1554
1522
|
---
|
|
1555
1523
|
|
|
1556
1524
|
<a id="ch12"></a>
|
|
1525
|
+
|
|
1557
1526
|
## Chapter 12 — Dereferencing and web-like semantics (`lib/deref.js`)
|
|
1558
1527
|
|
|
1559
1528
|
Some N3 workflows treat IRIs as pointers to more knowledge. Eyeling supports this with:
|
|
1560
1529
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1530
|
+
- `log:content` — fetch raw text
|
|
1531
|
+
- `log:semantics` — fetch and parse into a formula
|
|
1532
|
+
- `log:semanticsOrError` — produce either a formula or an error literal
|
|
1564
1533
|
|
|
1565
1534
|
`deref.js` is deliberately synchronous so the engine can remain synchronous.
|
|
1566
1535
|
|
|
1567
1536
|
### 12.1 Two environments: Node vs browser/worker
|
|
1568
1537
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
* in practice, any non-http IRI is treated as a local path for convenience.
|
|
1538
|
+
- In **Node**, dereferencing can read:
|
|
1539
|
+
- HTTP(S) via a subprocess that runs `fetch()` (keeps the engine synchronous)
|
|
1540
|
+
- local files (including `file://` URIs) via `fs.readFileSync`
|
|
1541
|
+
- in practice, any non-http IRI is treated as a local path for convenience.
|
|
1574
1542
|
|
|
1575
|
-
|
|
1543
|
+
- In **browser/worker**, dereferencing uses synchronous XHR (HTTP(S) only), subject to CORS.
|
|
1544
|
+
- Many browsers restrict synchronous XHR on the main thread; use a worker (as in `demo.html`) to avoid UI blocking.
|
|
1576
1545
|
|
|
1577
1546
|
### 12.2 Caching
|
|
1578
1547
|
|
|
1579
1548
|
Dereferencing is cached by IRI-without-fragment (fragments are stripped). There are separate caches for:
|
|
1580
1549
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1550
|
+
- raw content text
|
|
1551
|
+
- parsed semantics (GraphTerm)
|
|
1552
|
+
- semantics-or-error
|
|
1584
1553
|
|
|
1585
1554
|
This is both a performance and a stability feature: repeated `log:semantics` calls in backward proofs won’t keep refetching.
|
|
1586
1555
|
|
|
@@ -1591,6 +1560,7 @@ Eyeling can optionally rewrite `http://…` to `https://…` before dereferencin
|
|
|
1591
1560
|
---
|
|
1592
1561
|
|
|
1593
1562
|
<a id="ch13"></a>
|
|
1563
|
+
|
|
1594
1564
|
## Chapter 13 — Printing, proofs, and the user-facing output
|
|
1595
1565
|
|
|
1596
1566
|
Once reasoning is done (or as it happens in streaming mode), Eyeling converts derived facts back to N3.
|
|
@@ -1599,10 +1569,10 @@ Once reasoning is done (or as it happens in streaming mode), Eyeling converts de
|
|
|
1599
1569
|
|
|
1600
1570
|
Printing handles:
|
|
1601
1571
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1572
|
+
- compact qnames via `PrefixEnv`
|
|
1573
|
+
- `rdf:type` as `a`
|
|
1574
|
+
- `owl:sameAs` as `=`
|
|
1575
|
+
- nice formatting for lists and formulas
|
|
1606
1576
|
|
|
1607
1577
|
The printer is intentionally simple; it prints what Eyeling can parse.
|
|
1608
1578
|
|
|
@@ -1610,9 +1580,9 @@ The printer is intentionally simple; it prints what Eyeling can parse.
|
|
|
1610
1580
|
|
|
1611
1581
|
When enabled, Eyeling prints a compact comment block per derived triple:
|
|
1612
1582
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1583
|
+
- the derived triple
|
|
1584
|
+
- the instantiated rule body that was provable
|
|
1585
|
+
- the schematic forward rule that produced it
|
|
1616
1586
|
|
|
1617
1587
|
It’s a “why this triple holds” explanation, not a globally exported proof graph.
|
|
1618
1588
|
|
|
@@ -1627,6 +1597,7 @@ This is especially useful in interactive demos (and is the basis of the playgrou
|
|
|
1627
1597
|
---
|
|
1628
1598
|
|
|
1629
1599
|
<a id="ch14"></a>
|
|
1600
|
+
|
|
1630
1601
|
## Chapter 14 — Entry points: CLI, bundle exports, and npm API
|
|
1631
1602
|
|
|
1632
1603
|
Eyeling exposes itself in three layers.
|
|
@@ -1635,50 +1606,51 @@ Eyeling exposes itself in three layers.
|
|
|
1635
1606
|
|
|
1636
1607
|
The bundle contains the whole engine. The CLI path is the “canonical behavior”:
|
|
1637
1608
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1609
|
+
- parse input file
|
|
1610
|
+
- reason to closure
|
|
1611
|
+
- print derived triples or output strings
|
|
1612
|
+
- optional proof comments
|
|
1613
|
+
- optional streaming
|
|
1643
1614
|
|
|
1644
1615
|
#### 14.1.1 CLI options at a glance
|
|
1645
1616
|
|
|
1646
1617
|
The current CLI supports a small set of flags (see `lib/cli.js`):
|
|
1647
1618
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1619
|
+
- `-a`, `--ast` — print the parsed AST as JSON and exit.
|
|
1620
|
+
- `-d`, `--deterministic-skolem` — make `log:skolem` stable across runs.
|
|
1621
|
+
- `-e`, `--enforce-https` — rewrite `http://…` to `https://…` for dereferencing builtins.
|
|
1622
|
+
- `-p`, `--proof-comments` — include per-fact proof comment blocks in output.
|
|
1623
|
+
- `-r`, `--strings` — after reasoning, render only `log:outputString` values (ordered by subject key).
|
|
1624
|
+
- `-s`, `--super-restricted` — disable all builtins except `log:implies` / `log:impliedBy`.
|
|
1625
|
+
- `-t`, `--stream` — stream derived triples as soon as they are derived.
|
|
1626
|
+
- `-v`, `--version` — print version and exit.
|
|
1627
|
+
- `-h`, `--help` — show usage.
|
|
1657
1628
|
|
|
1658
1629
|
### 14.2 `lib/entry.js`: bundler-friendly exports
|
|
1659
1630
|
|
|
1660
1631
|
`lib/entry.js` exports:
|
|
1661
1632
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1633
|
+
- public APIs: `reasonStream`, `main`, `version`
|
|
1634
|
+
- plus a curated set of internals used by the demo (`lex`, `Parser`, `forwardChain`, etc.)
|
|
1664
1635
|
|
|
1665
1636
|
### 14.3 `index.js`: the npm API wrapper
|
|
1666
1637
|
|
|
1667
1638
|
The npm `reason(...)` function does something intentionally simple and robust:
|
|
1668
1639
|
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1640
|
+
- write your N3 input to a temp file
|
|
1641
|
+
- spawn the bundled CLI (`node eyeling.js ... input.n3`)
|
|
1642
|
+
- return stdout (and forward stderr)
|
|
1672
1643
|
|
|
1673
1644
|
This ensures the API matches the CLI perfectly and keeps the public surface small.
|
|
1674
1645
|
|
|
1675
1646
|
One practical implication:
|
|
1676
1647
|
|
|
1677
|
-
|
|
1648
|
+
- 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.
|
|
1678
1649
|
|
|
1679
1650
|
---
|
|
1680
1651
|
|
|
1681
1652
|
<a id="ch15"></a>
|
|
1653
|
+
|
|
1682
1654
|
## Chapter 15 — A worked example: Socrates, step by step
|
|
1683
1655
|
|
|
1684
1656
|
Consider:
|
|
@@ -1696,19 +1668,16 @@ Consider:
|
|
|
1696
1668
|
What Eyeling does:
|
|
1697
1669
|
|
|
1698
1670
|
1. Parsing yields two facts:
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
* premise goals: `?S a ?A`, `?A rdfs:subClassOf ?B`
|
|
1704
|
-
* head: `?S a ?B`
|
|
1671
|
+
- `(:Socrates rdf:type :Human)`
|
|
1672
|
+
- `(:Human rdfs:subClassOf :Mortal)` and one forward rule:
|
|
1673
|
+
- premise goals: `?S a ?A`, `?A rdfs:subClassOf ?B`
|
|
1674
|
+
- head: `?S a ?B`
|
|
1705
1675
|
|
|
1706
1676
|
2. Forward chaining scans the rule and calls `proveGoals` on the body.
|
|
1707
1677
|
|
|
1708
1678
|
3. Proving `?S a ?A` matches the first fact, producing `{ S = :Socrates, A = :Human }`.
|
|
1709
1679
|
|
|
1710
|
-
4. With that substitution, the second goal becomes `:Human rdfs:subClassOf ?B`.
|
|
1711
|
-
It matches the second fact, extending to `{ B = :Mortal }`.
|
|
1680
|
+
4. With that substitution, the second goal becomes `:Human rdfs:subClassOf ?B`. It matches the second fact, extending to `{ B = :Mortal }`.
|
|
1712
1681
|
|
|
1713
1682
|
5. Eyeling instantiates the head `?S a ?B` → `:Socrates a :Mortal`.
|
|
1714
1683
|
|
|
@@ -1719,6 +1688,7 @@ That’s the whole engine in miniature: unify, compose substitutions, emit head
|
|
|
1719
1688
|
---
|
|
1720
1689
|
|
|
1721
1690
|
<a id="ch16"></a>
|
|
1691
|
+
|
|
1722
1692
|
## Chapter 16 — Extending Eyeling (without breaking it)
|
|
1723
1693
|
|
|
1724
1694
|
Eyeling is small, which makes it pleasant to extend — but there are a few invariants worth respecting.
|
|
@@ -1727,20 +1697,20 @@ Eyeling is small, which makes it pleasant to extend — but there are a few inva
|
|
|
1727
1697
|
|
|
1728
1698
|
Most extensions belong in `lib/builtins.js` (inside `evalBuiltin`):
|
|
1729
1699
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1700
|
+
- Decide if your builtin is:
|
|
1701
|
+
- a test (0/1 solution)
|
|
1702
|
+
- functional (bind output)
|
|
1703
|
+
- generator (many solutions)
|
|
1704
|
+
- Return _deltas_ `{ varName: Term }`, not full substitutions.
|
|
1705
|
+
- Be cautious with fully-unbound cases: generators can explode the search space.
|
|
1706
|
+
- If you add a _new predicate_ (not just a new case inside an existing namespace), make sure it is recognized by `isBuiltinPred(...)`.
|
|
1737
1707
|
|
|
1738
1708
|
A small architectural note: `lib/builtins.js` is initialized by the engine via `makeBuiltins(deps)`. It receives hooks (unification, proving, deref, scoped-closure helpers, …) instead of importing the engine directly, which keeps the module graph acyclic and makes browser bundling easier.
|
|
1739
1709
|
|
|
1740
1710
|
If your builtin needs a stable view of the scoped closure, follow the scoped-builtin pattern:
|
|
1741
1711
|
|
|
1742
|
-
|
|
1743
|
-
|
|
1712
|
+
- read from `facts.__scopedSnapshot`
|
|
1713
|
+
- honor `facts.__scopedClosureLevel` and priority gating
|
|
1744
1714
|
|
|
1745
1715
|
And if your builtin is “forward-only” (needs inputs bound), it’s fine to **fail early** until inputs are available — forward rule proving enables builtin deferral, so the goal can be retried later in the same conjunction.
|
|
1746
1716
|
|
|
@@ -1748,23 +1718,24 @@ And if your builtin is “forward-only” (needs inputs bound), it’s fine to *
|
|
|
1748
1718
|
|
|
1749
1719
|
If you add a new Term subclass, you’ll likely need to touch:
|
|
1750
1720
|
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1721
|
+
- printing (`termToN3`)
|
|
1722
|
+
- unification and equality (`unifyTerm`, `termsEqual`, fast keys)
|
|
1723
|
+
- variable collection for compaction (`gcCollectVarsInTerm`)
|
|
1724
|
+
- groundness checks
|
|
1755
1725
|
|
|
1756
1726
|
### 16.3 Parser extensions
|
|
1757
1727
|
|
|
1758
1728
|
If you extend parsing, preserve the Rule invariants:
|
|
1759
1729
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1730
|
+
- rule premise is a triple list
|
|
1731
|
+
- rule conclusion is a triple list
|
|
1732
|
+
- blanks in premise are lifted (or handled consistently)
|
|
1733
|
+
- `headBlankLabels` must reflect blanks occurring explicitly in the head _before_ skolemization
|
|
1764
1734
|
|
|
1765
1735
|
---
|
|
1766
1736
|
|
|
1767
1737
|
<a id="epilogue"></a>
|
|
1738
|
+
|
|
1768
1739
|
## Epilogue: the philosophy of this engine
|
|
1769
1740
|
|
|
1770
1741
|
Eyeling’s codebase is compact because it chooses one powerful idea and leans into it:
|
|
@@ -1782,10 +1753,10 @@ Everything else is engineering detail — interesting, careful, sometimes subtle
|
|
|
1782
1753
|
---
|
|
1783
1754
|
|
|
1784
1755
|
<a id="app-a"></a>
|
|
1756
|
+
|
|
1785
1757
|
## Appendix A — Eyeling user notes
|
|
1786
1758
|
|
|
1787
|
-
This appendix is a compact, user-facing reference for **running Eyeling** and **writing inputs that work well**.
|
|
1788
|
-
For deeper explanations and implementation details, follow the chapter links in each section.
|
|
1759
|
+
This appendix is a compact, user-facing reference for **running Eyeling** and **writing inputs that work well**. For deeper explanations and implementation details, follow the chapter links in each section.
|
|
1789
1760
|
|
|
1790
1761
|
### A.1 Install and run
|
|
1791
1762
|
|
|
@@ -1809,10 +1780,10 @@ See also: [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
|
|
|
1809
1780
|
|
|
1810
1781
|
### A.2 What Eyeling prints
|
|
1811
1782
|
|
|
1812
|
-
By default, Eyeling prints **newly derived forward facts** (the heads of fired `=>` rules), serialized as N3.
|
|
1813
|
-
It does **not** reprint your input facts.
|
|
1783
|
+
By default, Eyeling prints **newly derived forward facts** (the heads of fired `=>` rules), serialized as N3. It does **not** reprint your input facts.
|
|
1814
1784
|
|
|
1815
1785
|
For proof/explanation output and output modes, see:
|
|
1786
|
+
|
|
1816
1787
|
- [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
|
|
1817
1788
|
|
|
1818
1789
|
### A.3 CLI quick reference
|
|
@@ -1824,6 +1795,7 @@ eyeling --help
|
|
|
1824
1795
|
```
|
|
1825
1796
|
|
|
1826
1797
|
Options:
|
|
1798
|
+
|
|
1827
1799
|
```
|
|
1828
1800
|
-a, --ast Print parsed AST as JSON and exit.
|
|
1829
1801
|
-d, --deterministic-skolem Make log:skolem stable across reasoning runs.
|
|
@@ -1837,6 +1809,7 @@ Options:
|
|
|
1837
1809
|
```
|
|
1838
1810
|
|
|
1839
1811
|
See also:
|
|
1812
|
+
|
|
1840
1813
|
- [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
|
|
1841
1814
|
- [Chapter 12 — Dereferencing and web-like semantics](#ch12)
|
|
1842
1815
|
|
|
@@ -1867,9 +1840,11 @@ Quoted graphs/formulas use `{ ... }`. Inside a quoted formula, directive scope m
|
|
|
1867
1840
|
- `@prefix/@base` and `PREFIX/BASE` directives may appear at top level **or inside `{ ... }`**, and apply to the formula they occur in (formula-local scoping).
|
|
1868
1841
|
|
|
1869
1842
|
For the formal grammar, see the N3 spec grammar:
|
|
1843
|
+
|
|
1870
1844
|
- [https://w3c.github.io/N3/spec/#grammar](https://w3c.github.io/N3/spec/#grammar)
|
|
1871
1845
|
|
|
1872
1846
|
See also:
|
|
1847
|
+
|
|
1873
1848
|
- [Chapter 4 — From characters to AST: lexing and parsing](#ch04)
|
|
1874
1849
|
|
|
1875
1850
|
### A.5 Builtins
|
|
@@ -1877,6 +1852,7 @@ See also:
|
|
|
1877
1852
|
Eyeling supports a built-in “standard library” across namespaces like `log:`, `math:`, `string:`, `list:`, `time:`, `crypto:`.
|
|
1878
1853
|
|
|
1879
1854
|
References:
|
|
1855
|
+
|
|
1880
1856
|
- W3C N3 Built-ins overview: [https://w3c.github.io/N3/reports/20230703/builtins.html](https://w3c.github.io/N3/reports/20230703/builtins.html)
|
|
1881
1857
|
- Eyeling implementation details: [Chapter 11 — Built-ins as a standard library](#ch11)
|
|
1882
1858
|
- The shipped builtin catalogue: `eyeling-builtins.ttl` (in this repo)
|
|
@@ -1888,42 +1864,49 @@ If you are running untrusted inputs, consider `--super-restricted` to disable al
|
|
|
1888
1864
|
When forward rule heads contain blank nodes (existentials), Eyeling replaces them with generated Skolem IRIs so derived facts are ground.
|
|
1889
1865
|
|
|
1890
1866
|
See:
|
|
1867
|
+
|
|
1891
1868
|
- [Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules](#ch09)
|
|
1892
1869
|
|
|
1893
1870
|
### A.7 Networking and `log:semantics`
|
|
1894
1871
|
|
|
1895
|
-
`log:content`, `log:semantics`, and related builtins dereference IRIs and parse retrieved content.
|
|
1896
|
-
This is powerful, but it is also I/O.
|
|
1872
|
+
`log:content`, `log:semantics`, and related builtins dereference IRIs and parse retrieved content. This is powerful, but it is also I/O.
|
|
1897
1873
|
|
|
1898
1874
|
See:
|
|
1875
|
+
|
|
1899
1876
|
- [Chapter 12 — Dereferencing and web-like semantics](#ch12)
|
|
1900
1877
|
|
|
1901
1878
|
Safety tip:
|
|
1902
|
-
|
|
1879
|
+
|
|
1880
|
+
- Use `--super-restricted` if you want to ensure _no_ dereferencing (and no other builtins) can run.
|
|
1903
1881
|
|
|
1904
1882
|
### A.8 Embedding Eyeling in JavaScript
|
|
1905
1883
|
|
|
1906
1884
|
If you depend on Eyeling as a library, the package exposes:
|
|
1885
|
+
|
|
1907
1886
|
- a CLI wrapper API (`reason(...)`), and
|
|
1908
1887
|
- in-process engine entry points (via the bundle exports).
|
|
1909
1888
|
|
|
1910
1889
|
See:
|
|
1890
|
+
|
|
1911
1891
|
- [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
|
|
1912
1892
|
|
|
1913
1893
|
### A.9 Further reading
|
|
1894
|
+
|
|
1914
1895
|
If you want to go deeper into N3 itself and the logic/programming ideas behind Eyeling, these are good starting points:
|
|
1915
1896
|
|
|
1916
1897
|
N3 / Semantic Web specs and reports:
|
|
1898
|
+
|
|
1917
1899
|
- [https://w3c.github.io/N3/spec/](https://w3c.github.io/N3/spec/)
|
|
1918
1900
|
- [https://w3c.github.io/N3/spec/builtins](https://w3c.github.io/N3/spec/builtins)
|
|
1919
1901
|
- [https://w3c.github.io/N3/spec/semantics](https://w3c.github.io/N3/spec/semantics)
|
|
1920
1902
|
|
|
1921
1903
|
Logic & reasoning background (Wikipedia):
|
|
1904
|
+
|
|
1922
1905
|
- [https://en.wikipedia.org/wiki/Mathematical_logic](https://en.wikipedia.org/wiki/Mathematical_logic)
|
|
1923
1906
|
- [https://en.wikipedia.org/wiki/Automated_reasoning](https://en.wikipedia.org/wiki/Automated_reasoning)
|
|
1924
1907
|
- [https://en.wikipedia.org/wiki/Forward_chaining](https://en.wikipedia.org/wiki/Forward_chaining)
|
|
1925
1908
|
- [https://en.wikipedia.org/wiki/Backward_chaining](https://en.wikipedia.org/wiki/Backward_chaining)
|
|
1926
|
-
- [https://en.wikipedia.org/wiki/
|
|
1909
|
+
- [https://en.wikipedia.org/wiki/Unification\_%28computer_science%29](https://en.wikipedia.org/wiki/Unification_%28computer_science%29)
|
|
1927
1910
|
- [https://en.wikipedia.org/wiki/Prolog](https://en.wikipedia.org/wiki/Prolog)
|
|
1928
1911
|
- [https://en.wikipedia.org/wiki/Datalog](https://en.wikipedia.org/wiki/Datalog)
|
|
1929
1912
|
- [https://en.wikipedia.org/wiki/Skolem_normal_form](https://en.wikipedia.org/wiki/Skolem_normal_form)
|