eyeling 1.11.4 → 1.11.5
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 +76 -19
- package/package.json +1 -1
package/HANDBOOK.md
CHANGED
|
@@ -109,22 +109,24 @@ If you want to follow the code in the same order Eyeling “thinks”, read:
|
|
|
109
109
|
2. `lib/lexer.js` — N3/Turtle-ish tokenization.
|
|
110
110
|
3. `lib/parser.js` — parsing tokens into triples, formulas, and rules.
|
|
111
111
|
4. `lib/rules.js` — small rule “compiler passes” (blank lifting, constraint delaying).
|
|
112
|
-
5. `lib/engine.js` — the core engine:
|
|
112
|
+
5. `lib/engine.js` — the core inference engine:
|
|
113
113
|
- equality + alpha equivalence for formulas
|
|
114
114
|
- unification + substitutions
|
|
115
115
|
- indexing facts and backward rules
|
|
116
|
-
- backward goal proving (`proveGoals`)
|
|
117
|
-
- forward saturation (`forwardChain`)
|
|
118
|
-
- built-ins (`evalBuiltin`)
|
|
116
|
+
- backward goal proving (`proveGoals`) and forward saturation (`forwardChain`)
|
|
119
117
|
- scoped-closure machinery (for `log:*In` and includes tests)
|
|
120
|
-
- explanations and output construction
|
|
121
118
|
- tracing hooks (`lib/trace.js`, `log:trace`)
|
|
122
119
|
- time helpers for `time:*` built-ins (`lib/time.js`)
|
|
123
120
|
- deterministic Skolem IDs (head existentials + `log:skolem`) (`lib/skolem.js`)
|
|
124
|
-
6. `lib/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
6. `lib/builtins.js` — builtin predicate evaluation plus shared literal/number/string/list helpers:
|
|
122
|
+
- `makeBuiltins(deps)` dependency-injects engine hooks (unification, proving, deref, …)
|
|
123
|
+
- exports `evalBuiltin(...)` and `isBuiltinPred(...)` back to the engine
|
|
124
|
+
- 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
|
+
7. `lib/explain.js` — proof comments + `log:outputString` aggregation (fact ordering and pretty output).
|
|
126
|
+
8. `lib/deref.js` — synchronous dereferencing for `log:content` / `log:semantics` (used by builtins and engine).
|
|
127
|
+
9. `lib/printing.js` — conversion back to N3 text.
|
|
128
|
+
10. `lib/cli.js` + `lib/entry.js` — command-line wiring and bundle entry exports.
|
|
129
|
+
11. `index.js` — the npm API wrapper (spawns the bundled CLI synchronously).
|
|
128
130
|
|
|
129
131
|
This is almost literally a tiny compiler pipeline:
|
|
130
132
|
|
|
@@ -299,6 +301,30 @@ Eyeling treats these as “constraints” and moves them to the *end* of a forwa
|
|
|
299
301
|
|
|
300
302
|
It’s not logically necessary, but it improves the chance that constraints run with variables already grounded, reducing wasted search.
|
|
301
303
|
|
|
304
|
+
### 5.3 Materializing anonymous RDF collections into N3 list terms
|
|
305
|
+
|
|
306
|
+
Many N3 documents encode lists using RDF’s linked-list vocabulary:
|
|
307
|
+
|
|
308
|
+
```n3
|
|
309
|
+
_:c rdf:first :a.
|
|
310
|
+
_:c rdf:rest _:d.
|
|
311
|
+
_:d rdf:first :b.
|
|
312
|
+
_:d rdf:rest rdf:nil.
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Eyeling supports *both* representations:
|
|
316
|
+
|
|
317
|
+
* **Concrete N3 lists** like `(:a :b)` are parsed as `ListTerm([...])` directly.
|
|
318
|
+
* **RDF collections** using `rdf:first`/`rdf:rest` can be traversed by list-aware builtins.
|
|
319
|
+
|
|
320
|
+
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:
|
|
321
|
+
|
|
322
|
+
* scans the **input triples** for well‑formed `rdf:first`/`rdf:rest` chains,
|
|
323
|
+
* **rewrites only anonymous (blank-node) list nodes** into concrete `ListTerm(...)`,
|
|
324
|
+
* and applies that rewrite consistently across the input triple set and all rule premises/heads.
|
|
325
|
+
|
|
326
|
+
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.
|
|
327
|
+
|
|
302
328
|
---
|
|
303
329
|
|
|
304
330
|
<a id="ch06"></a>
|
|
@@ -446,7 +472,7 @@ Eyeling’s order is intentional: built-ins often bind variables cheaply; rules
|
|
|
446
472
|
|
|
447
473
|
### 8.3 Built-ins: return *deltas*, not full substitutions
|
|
448
474
|
|
|
449
|
-
A built-in is evaluated
|
|
475
|
+
A built-in is evaluated by the engine via the builtin library in `lib/builtins.js`:
|
|
450
476
|
|
|
451
477
|
```js
|
|
452
478
|
deltas = evalBuiltin(goal0, {}, facts, backRules, ...)
|
|
@@ -454,9 +480,18 @@ for delta in deltas:
|
|
|
454
480
|
composed = composeSubst(currentSubst, delta)
|
|
455
481
|
```
|
|
456
482
|
|
|
457
|
-
So built-ins behave like relations that can generate zero, one, or many possible bindings.
|
|
483
|
+
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.
|
|
484
|
+
|
|
485
|
+
#### 8.3.1 Builtin deferral and “vacuous” solutions
|
|
458
486
|
|
|
459
|
-
|
|
487
|
+
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).
|
|
488
|
+
|
|
489
|
+
“Can’t make progress” includes both cases:
|
|
490
|
+
|
|
491
|
+
- the builtin returns **no solutions** (`[]`), and
|
|
492
|
+
- the builtin returns only **vacuous solutions** (`[{}]`, i.e., success with *no new bindings*) while the goal still contains unbound vars/blanks.
|
|
493
|
+
|
|
494
|
+
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.
|
|
460
495
|
|
|
461
496
|
### 8.4 Loop prevention: a simple visited list
|
|
462
497
|
|
|
@@ -636,10 +671,12 @@ This makes formulas a little world you can reason about as data.
|
|
|
636
671
|
---
|
|
637
672
|
|
|
638
673
|
<a id="ch11"></a>
|
|
639
|
-
## Chapter 11 — Built-ins as a standard library (`
|
|
674
|
+
## Chapter 11 — Built-ins as a standard library (`lib/builtins.js`)
|
|
640
675
|
|
|
641
676
|
Built-ins are where Eyeling stops being “just a Datalog engine” and becomes a practical N3 tool.
|
|
642
677
|
|
|
678
|
+
Implementation note: builtin code lives in `lib/builtins.js` and is wired into the prover by the engine via `makeBuiltins(deps)` (dependency injection keeps the modules loosely coupled).
|
|
679
|
+
|
|
643
680
|
### 11.1 How Eyeling recognizes built-ins
|
|
644
681
|
|
|
645
682
|
A predicate is treated as builtin if:
|
|
@@ -664,7 +701,7 @@ That means built-ins can be:
|
|
|
664
701
|
|
|
665
702
|
List operations are a common source of generators; numeric comparisons are tests.
|
|
666
703
|
|
|
667
|
-
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/
|
|
704
|
+
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).
|
|
668
705
|
|
|
669
706
|
---
|
|
670
707
|
|
|
@@ -1446,7 +1483,7 @@ From a logic-programming point of view, printing is awkward: if you print *durin
|
|
|
1446
1483
|
The predicate `log:outputString` is the only officially supported “side-effect channel”, and even it is handled in two phases:
|
|
1447
1484
|
|
|
1448
1485
|
1. **During reasoning (declarative phase):**
|
|
1449
|
-
`log:outputString` behaves like a constraint-style builtin: it succeeds when its arguments are well-formed and sufficiently bound (notably, when the object is a string literal that can be emitted). Importantly, it does *not* print anything at this time. If a rule derives a triple like:
|
|
1486
|
+
`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:
|
|
1450
1487
|
|
|
1451
1488
|
```n3
|
|
1452
1489
|
:k log:outputString "Hello\n".
|
|
@@ -1455,7 +1492,7 @@ The predicate `log:outputString` is the only officially supported “side-effect
|
|
|
1455
1492
|
then that triple simply becomes part of the fact base like any other fact.
|
|
1456
1493
|
|
|
1457
1494
|
2. **After reasoning (rendering phase):**
|
|
1458
|
-
Once saturation finishes, Eyeling scans the *final closure* for `log:outputString` facts and renders them deterministically. Concretely, the CLI collects all such triples, orders them in a stable way (using the subject as a key so output order is reproducible), and concatenates their string objects into the final emitted text.
|
|
1495
|
+
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.
|
|
1459
1496
|
|
|
1460
1497
|
This separation is not just an aesthetic choice; it preserves the meaning of logic search:
|
|
1461
1498
|
|
|
@@ -1530,6 +1567,8 @@ When enabled, Eyeling prints a compact comment block per derived triple:
|
|
|
1530
1567
|
|
|
1531
1568
|
It’s a “why this triple holds” explanation, not a globally exported proof graph.
|
|
1532
1569
|
|
|
1570
|
+
Implementation note: the engine records lightweight `DerivedFact` objects during forward chaining, and `lib/explain.js` (via `makeExplain(...)`) is responsible for turning those objects into the human-readable proof comment blocks.
|
|
1571
|
+
|
|
1533
1572
|
### 13.3 Streaming derived facts
|
|
1534
1573
|
|
|
1535
1574
|
The engine’s `reasonStream` API can accept an `onDerived` callback. Each time a new forward fact is derived, Eyeling can report it immediately.
|
|
@@ -1553,6 +1592,20 @@ The bundle contains the whole engine. The CLI path is the “canonical behavior
|
|
|
1553
1592
|
* optional proof comments
|
|
1554
1593
|
* optional streaming
|
|
1555
1594
|
|
|
1595
|
+
#### 14.1.1 CLI options at a glance
|
|
1596
|
+
|
|
1597
|
+
The current CLI supports a small set of flags (see `lib/cli.js`):
|
|
1598
|
+
|
|
1599
|
+
* `-a`, `--ast` — print the parsed AST as JSON and exit.
|
|
1600
|
+
* `-d`, `--deterministic-skolem` — make `log:skolem` stable across runs.
|
|
1601
|
+
* `-e`, `--enforce-https` — rewrite `http://…` to `https://…` for dereferencing builtins.
|
|
1602
|
+
* `-p`, `--proof-comments` — include per-fact proof comment blocks in output.
|
|
1603
|
+
* `-r`, `--strings` — after reasoning, render only `log:outputString` values (ordered by subject key).
|
|
1604
|
+
* `-s`, `--super-restricted` — disable all builtins except `log:implies` / `log:impliedBy`.
|
|
1605
|
+
* `-t`, `--stream` — stream derived triples as soon as they are derived.
|
|
1606
|
+
* `-v`, `--version` — print version and exit.
|
|
1607
|
+
* `-h`, `--help` — show usage.
|
|
1608
|
+
|
|
1556
1609
|
### 14.2 `lib/entry.js`: bundler-friendly exports
|
|
1557
1610
|
|
|
1558
1611
|
`lib/entry.js` exports:
|
|
@@ -1623,21 +1676,25 @@ Eyeling is small, which makes it pleasant to extend — but there are a few inva
|
|
|
1623
1676
|
|
|
1624
1677
|
### 16.1 Adding a builtin
|
|
1625
1678
|
|
|
1626
|
-
Most extensions belong in `evalBuiltin
|
|
1679
|
+
Most extensions belong in `lib/builtins.js` (inside `evalBuiltin`):
|
|
1627
1680
|
|
|
1628
1681
|
* Decide if your builtin is:
|
|
1629
|
-
|
|
1630
1682
|
* a test (0/1 solution)
|
|
1631
1683
|
* functional (bind output)
|
|
1632
1684
|
* generator (many solutions)
|
|
1633
1685
|
* Return *deltas* `{ varName: Term }`, not full substitutions.
|
|
1634
1686
|
* Be cautious with fully-unbound cases: generators can explode the search space.
|
|
1687
|
+
* If you add a *new predicate* (not just a new case inside an existing namespace), make sure it is recognized by `isBuiltinPred(...)`.
|
|
1635
1688
|
|
|
1636
|
-
|
|
1689
|
+
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.
|
|
1690
|
+
|
|
1691
|
+
If your builtin needs a stable view of the scoped closure, follow the scoped-builtin pattern:
|
|
1637
1692
|
|
|
1638
1693
|
* read from `facts.__scopedSnapshot`
|
|
1639
1694
|
* honor `facts.__scopedClosureLevel` and priority gating
|
|
1640
1695
|
|
|
1696
|
+
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.
|
|
1697
|
+
|
|
1641
1698
|
### 16.2 Adding new term shapes
|
|
1642
1699
|
|
|
1643
1700
|
If you add a new Term subclass, you’ll likely need to touch:
|