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 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 *code* and as a *reasoning machine*.
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 *and* for built-in predicates.
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: *forward rules are executed by proving their bodies using a backward engine*. This lets forward rules depend on computations and “virtual predicates” without explicitly materializing everything as facts.
48
+ 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 *anonymous* `rdf:first`/`rdf:rest` linked lists into concrete N3 list terms so `list:*` builtins can work uniformly
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 *explicitly in the head* of a rule. Those blanks are treated as existentials and get skolemized per firing. (Chapter 9.)
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 *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)
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
- * `true` becomes “empty triple list”
269
- * `false` becomes “no head triples” *plus* the `isFuse` flag if forward.
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 *rule premises* behave like universally-quantified placeholders. Eyeling implements this by converting `Blank(label)` to `Var(_bN)` in the premise only.
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 *not* lifted — they remain blanks and later become existentials (Chapter 9).
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
- * `math:greaterThan`, `math:lessThan`, `math:equalTo`, …
303
- * `string:matches`, `string:contains`, …
304
- * `log:notIncludes`, `log:forAllIn`, `log:outputString`, …
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 *end* of a forward rule premise. This is a Prolog-style heuristic:
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 *both* representations:
329
+ Eyeling supports _both_ representations:
324
330
 
325
- * **Concrete N3 lists** like `(:a :b)` are parsed as `ListTerm([...])` directly.
326
- * **RDF collections** using `rdf:first`/`rdf:rest` can be traversed by list-aware builtins.
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
- * scans the **input triples** for well‑formed `rdf:first`/`rdf:rest` chains,
331
- * **rewrites only anonymous (blank-node) list nodes** into concrete `ListTerm(...)`,
332
- * and applies that rewrite consistently across the input triple set and all rule premises/heads.
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
- * equality and normalization (especially for literals)
344
- * alpha-equivalence for formulas
345
- * unification and substitution application
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
- * `{ _:x :p ?y. }` should match `{ _:z :p ?w. }`
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 *groundness*:
368
+ Eyeling makes a deliberate choice about _groundness_:
362
369
 
363
- * a triple is “ground” if it has no free variables in normal positions
364
- * **variables inside a `GraphTerm` do not make the surrounding triple non-ground**
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 *mention formulas with variables* as data.
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
- * if `X → Var(Y)` and `Y → Iri(...)`, applying to `X` yields the IRI.
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 *delta* objects (small `{ varName: Term }` maps).
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
- * keep one **mutable** substitution object during DFS
386
- * when a candidate match yields a delta, **apply the bindings in place**
387
- * record newly-bound variable names on a **trail stack**
388
- * on backtracking, **undo** only the bindings pushed since a saved “mark”
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
- * **`applySubstTerm` is the only “chain chaser”.** It follows `Var → Term` links until it reaches a stable term.
395
- * Unification’s occurs-check prevents most cycles, but `applySubstTerm` still defends against accidental cyclic chains.
396
- * The cycle guard is written to avoid allocating a `Set` in the common case (short chains).
397
- * **Structural sharing is deliberate.** Applying a substitution often changes nothing:
398
- * `applySubstTerm` returns the original term when it is unaffected.
399
- * list/open-list/graph terms are only rebuilt if at least one component changes (lazy copy-on-change).
400
- * `applySubstTriple` returns the original `Triple` when `s/p/o` are unchanged.
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
- * variable binding with occurs check
409
- * list unification (elementwise)
410
- * open-list unification (prefix + tail variable)
411
- * formula unification via graph unification:
412
-
413
- * fast path: identical triple list
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 *set-like*: order doesn’t matter.
419
- 2. It’s *substitution-threaded*: choices made while matching one triple restrict the remaining matches, just like Prolog.
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
- * `literalParts(lit)` splits lexical form and datatype IRI
426
- * it recognizes RDF JSON datatype (`rdf:JSON` / `<...rdf#JSON>`)
427
- * it includes caches for numeric parsing, integer parsing (`BigInt`), and numeric metadata.
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
- * `facts.__byPred: Map<predicateId, number[]>` where each entry is an index into `facts` (and `predicateId` is `predicate.__tid`)
445
- * `facts.__byPS: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
446
- * `facts.__byPO: Map<predicateId, Map<termId, number[]>>` where each entry is an index into `facts` (and `termId` is `term.__tid`)
447
- * `facts.__keySet: Set<string>` for a fast-path `"sid pid oid"` key (all three are `__tid` values)
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 *not* interchangeable; the blank **label** (and thus its `__tid`) is part of the key.
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
- * `goals`: remaining goal triples
484
- * `subst`: current substitution
485
- * `depth`: current depth (used for compaction heuristics)
486
- * `visited`: previously-seen goals (loop prevention)
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
- * take the first goal
496
- * apply the current substitution to it
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 *deltas*, not full substitutions
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 *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).
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 *no new bindings*) while the goal still contains unbound vars/blanks.
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
- * `rules whose head predicate is p`
545
- * plus `__wildHeadPred` for rules whose head predicate is not an IRI (rare, but supported)
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 *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.
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 *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.
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
- * Eyeling only needs **one** proof of the body.
602
- * And if all head triples are already known, it can skip proving the body entirely.
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
- * `_:sk_0`, `_:sk_1`, …
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 *same* firing instance doesn’t keep generating new blanks across outer iterations.
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
- * Eyeling proves the premise (it only needs one solution)
627
- * if the premise is provable, it prints a message and exits with status code 2
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 *new rules*:
639
+ Eyeling treats certain derived triples as _new rules_:
634
640
 
635
- * `log:implies` and `log:impliedBy` where subject/object are formulas
636
- * it also accepts the literal `true` as an empty formula `{}` on either side
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 *promotes* it into a live rule by constructing a new `Rule` object and inserting it into the forward or backward rule list.
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 *evolving*.
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 *outer loop* that alternates:
674
+ Forward chaining runs inside an _outer loop_ that alternates:
668
675
 
669
- * **Phase A**: scoped built-ins are disabled (they “delay” by failing)
676
+ - **Phase A**: scoped built-ins are disabled (they “delay” by failing)
670
677
 
671
- * Eyeling saturates normally to a fixpoint
678
+ - Eyeling saturates normally to a fixpoint
672
679
 
673
- * then Eyeling freezes a snapshot of the saturated facts
680
+ - then Eyeling freezes a snapshot of the saturated facts
674
681
 
675
- * **Phase B**: scoped built-ins are enabled, but they query only the frozen snapshot
682
+ - **Phase B**: scoped built-ins are enabled, but they query only the frozen snapshot
676
683
 
677
- * Eyeling runs saturation again (new facts can appear due to scoped queries)
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
- * level 0 means “no snapshot available” (Phase A)
692
- * level 1, 2, … correspond to snapshots produced after each Phase A saturation
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
- * `log:collectAllIn` and `log:forAllIn` use the **object position** for priority
697
- * `log:includes` and `log:notIncludes` use the **subject position** for priority
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
- * given a formula `{ ... }` (a `GraphTerm`),
708
- * Eyeling computes the deductive closure *inside that formula*:
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
- * extract rule triples inside it (`log:implies`, `log:impliedBy`)
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
- * it is an IRI in one of the builtin namespaces:
738
+ - it is an IRI in one of the builtin namespaces:
739
+ - `crypto:`, `math:`, `log:`, `string:`, `time:`, `list:`
732
740
 
733
- * `crypto:`, `math:`, `log:`, `string:`, `time:`, `list:`
734
- * or it is `rdf:first` / `rdf:rest` (treated as list-like builtins)
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 *deltas*.
748
+ Every builtin returns a list of substitution _deltas_.
742
749
 
743
750
  That means built-ins can be:
744
751
 
745
- * **functional** (return one delta binding an output)
746
- * **tests** (return either `[{}]` for success or `[]` for failure)
747
- * **generators** (return many deltas)
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 *fully self-contained* and to cover **every builtin currently implemented in `lib/builtins.js`** (including the `rdf:first` / `rdf:rest` aliases).
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 *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.
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 *that*?”:
766
+ That one sentence explains a lot of “why does it behave like _that_?”:
760
767
 
761
- * Builtins are evaluated **during backward proof** (goal solving), just like facts and backward rules.
762
- * A builtin may produce **zero solutions** (fail), **one solution** (deterministic succeed), or **many solutions** (a generator).
763
- * Most builtins behave like relations, not like functions: they can sometimes run “backwards” (bind the subject from the object) if the implementation supports it.
764
- * 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.
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
- * `$s+` / `$o+` — input must be bound (or at least not a variable in practice)
771
- * `$s-` / `$o-` — output position (often a variable that will be bound)
772
- * `$s?` / `$o?` — may be unbound
773
- * `$s.i` — list element *i* inside the subject list
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
- * The **subject must be a literal**. Eyeling takes the literal’s lexical form (stripping quotes) as UTF-8 input.
797
- * The **object** is unified with a **plain string literal** containing the hex digest.
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
- * `math:greaterThan` (>)
825
- * `math:lessThan` (<)
826
- * `math:notGreaterThan` (≤)
827
- * `math:notLessThan` (≥)
828
- * `math:equalTo` (=)
829
- * `math:notEqualTo` (≠)
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
- * Proper XSD numeric literals (`xsd:integer`, `xsd:decimal`, `xsd:float`, `xsd:double`, and integer-derived types).
847
- * Untyped numeric tokens (`123`, `-4.5`, `1.2e3`) when they look numeric.
848
- * `xsd:duration` literals (treated as seconds via a simplified model).
849
- * `xsd:date` and `xsd:dateTime` literals (converted to epoch seconds for comparison).
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
- * `NaN` is treated as **not equal to anything**, including itself, for `math:equalTo`.
854
- * Comparisons involving non-parsable values simply fail.
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
- * Subject must be a list of **at least two** numeric terms.
869
- * Computes the numeric sum.
870
- * 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.
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
- * Same conventions as `math:sum`, but multiplies.
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
- * Parses both inputs as numbers.
901
- * Requires finite values and `b != 0`.
902
- * Computes `a / b`, picking a suitable numeric datatype for output.
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
- * Intended for integer division with remainder discarded (truncation toward zero).
909
- * Prefers exact arithmetic using **BigInt** if both inputs are integer literals.
910
- * Falls back to Number parsing if needed, but still requires integer-like values.
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
- * Integer-only modulus.
917
- * Uses BigInt when possible; otherwise requires both numbers to still represent integers.
918
- * Fails on division by zero.
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
- * Rounds to nearest integer.
925
- * Tie-breaking follows JavaScript `Math.round`, i.e. halves go toward **+∞** (`-1.5 -> -1`, `1.5 -> 2`).
926
- * Eyeling emits the integer as an **integer token literal** (and also accepts typed numerics if they compare equal).
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
- * Forward direction: if base and exponent are numeric, computes `base ** exp`.
937
- * Reverse direction (limited): Eyeling can sometimes solve for the exponent if:
938
-
939
- * base and result are numeric, finite, and **positive**
940
- * base is not 1
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
- * `math:absoluteValue`
951
- * `math:negation`
952
- * `math:degrees` (and implicitly its inverse “radians” conversion)
953
- * `math:sin`, `math:cos`, `math:tan`
954
- * `math:asin`, `math:acos`, `math:atan`
955
- * `math:sinh`, `math:cosh`, `math:tanh` (only if JS provides the functions)
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
- * `time:year`
977
- * `time:month`
978
- * `time:day`
979
- * `time:hour`
980
- * `time:minute`
981
- * `time:second`
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
- * Subject must be an `xsd:dateTime` literal in a format Eyeling can parse.
989
- * Object becomes the corresponding integer component (as an integer token literal).
990
- * If the object is already a numeric literal, Eyeling accepts it if it matches.
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
- * `"Z"` for UTC, or
1000
- * a string like `"+02:00"` / `"-05:00"`
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 *materializes* such structures into internal list terms before reasoning so that `list:*` builtins can operate uniformly.
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
- * `rdf:first` as an alias of `list:first`
1029
- * `rdf:rest` as an alias of `list:rest`
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
- * Succeeds iff the subject is a **non-empty closed list**.
1039
- * Unifies the object with the first element.
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
- * closed lists `(a b c)`, and
1049
- * *open lists* of the form `(a b ... ?T)` internally.
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
- * Rest of `(a ... ?T)` is `?T`
1054
- * Rest of `(a b ... ?T)` is `(b ... ?T)`
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
- * If the object is `(first restList)`, it can construct the list.
1066
- * If `rest` is a variable, Eyeling constructs an open list term.
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
- * `(0 a)`, `(1 b)`, `(2 c)`, …
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
- * If the index is bound, it can return the value.
1119
- * If the value is bound, it can search for indices that match.
1120
- * If both are variables, it generates pairs (similar to `iterate`, but with separate index/value logic).
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
- * If subject is a list, object becomes its reversal.
1149
- * If object is a list, subject becomes its reversal.
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
- * Requires the input list’s elements to be **ground**.
1186
- * Orders literals numerically when both sides look numeric; otherwise compares their lexical strings.
1187
- * Orders lists lexicographically by elements.
1188
- * Orders IRIs by IRI string.
1189
- * Falls back to a stable structural key for mixed cases.
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 *the full engine* (facts, backward rules, and builtins).
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 *meta-logic*. Eyeling supports the core operators you need to treat formulas as terms, reason inside quoted graphs, and compute closures.
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
- * Subject is a list of formulas.
1245
- * Object becomes a formula containing all triples from all inputs.
1246
- * Duplicate triples are removed.
1247
- * The literal `true` is treated as the **empty formula** and is ignored in the merge.
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 *deductive closure* of the formula `F` **using only the information inside `F`**:
1238
+ Computes the _deductive closure_ of the formula `F` **using only the information inside `F`**:
1255
1239
 
1256
- * Eyeling starts with all triples inside `F` as facts.
1257
- * It treats `{A} => {B}` (represented internally as a `log:implies` triple between formulas) as a forward rule.
1258
- * It treats `{A} <= {B}` as the corresponding forward direction for closure purposes.
1259
- * Then it forward-chains to a fixpoint *within that local fact set*.
1260
- * The result is returned as a formula containing all derived triples.
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
- * Dereferences the IRI (fragment stripped) and returns the raw bytes as an `xsd:string` literal.
1274
- * In Node: HTTP(S) is fetched synchronously; non-HTTP is treated as a local file path (including `file://`).
1275
- * In browsers/workers: uses synchronous XHR (subject to CORS).
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 *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.
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
- * `error(dereference_failed,...)`
1291
- * `error(parse_error,...)`
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
- * `log:Formula` (quoted graph)
1309
- * `log:Literal`
1310
- * `rdf:List` (closed or open list terms)
1311
- * `log:Other` (IRIs, blank nodes, etc.)
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
- * If object is a literal, it can produce the subject list `(stringLiteral datatypeIri)`.
1322
- * If subject is such a list, it can produce the corresponding datatype literal.
1323
- * If both subject and object are variables, Eyeling treats this as satisfiable and succeeds once.
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
- * If object is `"hello"@en`, subject can become `("hello" "en")`.
1332
- * If subject is `("hello" "en")`, object can become `"hello"@en`.
1333
- * Fully unbound succeeds once.
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 *syntax*, Eyeling parses `{A} => {B}` and `{A} <= {B}` into internal forward/backward rules.
1320
+ As _syntax_, Eyeling parses `{A} => {B}` and `{A} <= {B}` into internal forward/backward rules.
1340
1321
 
1341
- As *builtins*, `log:implies` and `log:impliedBy` let you **inspect the currently loaded rule set**:
1322
+ As _builtins_, `log:implies` and `log:impliedBy` let you **inspect the currently loaded rule set**:
1342
1323
 
1343
- * `log:implies` enumerates forward rules as `(premiseFormula, conclusionFormula)` pairs.
1344
- * `log:impliedBy` enumerates backward rules similarly.
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
- * Eyeling reasons *only inside that formula* (its triples are the fact store).
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
- * Eyeling uses a *frozen snapshot* of the current global closure.
1367
- * The “priority” is read from the subject if it’s a positive integer literal `N`.
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
- * The object may be the literal `true`, meaning the empty formula, which is always included (subject to the priority gating above).
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
- * Proves `WhereFormula` in the chosen scope.
1386
- * For each solution, applies it to `ValueTemplate` and collects the instantiated terms into a list.
1387
- * Unifies `OutList` with that list.
1388
- * If `OutList` is a blank node, Eyeling just checks satisfiable without binding/collecting.
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 *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.
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
- * If subject is an IRI, object can be unified with a string literal of its IRI.
1415
- * 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.
1416
- * Some “fully unbound / don’t-care” combinations succeed once to avoid infinite enumeration.
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
- * When you run Eyeling with `--strings` / `-r`, the CLI collects all `log:outputString` triples from the *saturated* closure.
1437
- * It sorts them deterministically by the subject “key” and concatenates the string values in that order.
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
- * Any **IRI** can be cast to a string (its IRI text).
1448
- * Any **literal** can be cast to a string:
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
- * quoted lexical forms decode N3/Turtle escapes,
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
- * Supports only `%s` and `%%`.
1471
- * Any other specifier (`%d`, `%f`, …) causes the builtin to fail.
1472
- * Missing arguments are treated as empty strings.
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
- * `string:contains`
1477
- * `string:containsIgnoringCase`
1478
- * `string:startsWith`
1479
- * `string:endsWith`
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
- * `string:equalIgnoringCase`
1486
- * `string:notEqualIgnoringCase`
1458
+ - `string:equalIgnoringCase`
1459
+ - `string:notEqualIgnoringCase`
1487
1460
 
1488
1461
  ### Lexicographic comparisons (constraints)
1489
1462
 
1490
- * `string:greaterThan`
1491
- * `string:lessThan`
1492
- * `string:notGreaterThan` (≤ in Unicode codepoint order)
1493
- * `string:notLessThan` (≥ in Unicode codepoint order)
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
- * If the pattern uses Unicode property escapes (like `\p{L}`) or code point escapes (`\u{...}`), Eyeling enables the `/u` flag.
1502
- * In Unicode mode, some “identity escapes” that would be SyntaxErrors in JS are sanitized in a conservative way.
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
- * Compiles `pattern` as a global regex (`/g`).
1517
- * Uses JavaScript replacement semantics (so `$1`, `$2`, etc. work).
1518
- * Returns the replaced string.
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 *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**.
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 *not* print anything at this time. If a rule derives a triple like:
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
- * 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.
1549
- * 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.
1550
- * Output becomes compositional. You can reason about output strings (e.g., sort them, filter them, derive them conditionally) just like any other data.
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 *declarative output fact* whose concrete rendering is a final, deterministic post-processing step.
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
- * `log:content` — fetch raw text
1562
- * `log:semantics` — fetch and parse into a formula
1563
- * `log:semanticsOrError` — produce either a formula or an error literal
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
- * In **Node**, dereferencing can read:
1570
-
1571
- * HTTP(S) via a subprocess (still synchronous)
1572
- * local files (including `file://` URIs) via `fs.readFileSync`
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
- * In **browser/worker**, dereferencing uses synchronous XHR, subject to CORS, and only for HTTP(S).
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
- * raw content text
1582
- * parsed semantics (GraphTerm)
1583
- * semantics-or-error
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
- * compact qnames via `PrefixEnv`
1603
- * `rdf:type` as `a`
1604
- * `owl:sameAs` as `=`
1605
- * nice formatting for lists and formulas
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
- * the derived triple
1614
- * the instantiated rule body that was provable
1615
- * the schematic forward rule that produced it
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
- * parse input file
1639
- * reason to closure
1640
- * print derived triples or output strings
1641
- * optional proof comments
1642
- * optional streaming
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
- * `-a`, `--ast` — print the parsed AST as JSON and exit.
1649
- * `-d`, `--deterministic-skolem` — make `log:skolem` stable across runs.
1650
- * `-e`, `--enforce-https` — rewrite `http://…` to `https://…` for dereferencing builtins.
1651
- * `-p`, `--proof-comments` — include per-fact proof comment blocks in output.
1652
- * `-r`, `--strings` — after reasoning, render only `log:outputString` values (ordered by subject key).
1653
- * `-s`, `--super-restricted` — disable all builtins except `log:implies` / `log:impliedBy`.
1654
- * `-t`, `--stream` — stream derived triples as soon as they are derived.
1655
- * `-v`, `--version` — print version and exit.
1656
- * `-h`, `--help` — show usage.
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
- * public APIs: `reasonStream`, `main`, `version`
1663
- * plus a curated set of internals used by the demo (`lex`, `Parser`, `forwardChain`, etc.)
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
- * write your N3 input to a temp file
1670
- * spawn the bundled CLI (`node eyeling.js ... input.n3`)
1671
- * return stdout (and forward stderr)
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
- * 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.
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
- * `(:Socrates rdf:type :Human)`
1701
- * `(:Human rdfs:subClassOf :Mortal)`
1702
- and one forward rule:
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
- * Decide if your builtin is:
1731
- * a test (0/1 solution)
1732
- * functional (bind output)
1733
- * generator (many solutions)
1734
- * Return *deltas* `{ varName: Term }`, not full substitutions.
1735
- * Be cautious with fully-unbound cases: generators can explode the search space.
1736
- * If you add a *new predicate* (not just a new case inside an existing namespace), make sure it is recognized by `isBuiltinPred(...)`.
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
- * read from `facts.__scopedSnapshot`
1743
- * honor `facts.__scopedClosureLevel` and priority gating
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
- * printing (`termToN3`)
1752
- * unification and equality (`unifyTerm`, `termsEqual`, fast keys)
1753
- * variable collection for compaction (`gcCollectVarsInTerm`)
1754
- * groundness checks
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
- * rule premise is a triple list
1761
- * rule conclusion is a triple list
1762
- * blanks in premise are lifted (or handled consistently)
1763
- * `headBlankLabels` must reflect blanks occurring explicitly in the head *before* skolemization
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
- - Use `--super-restricted` if you want to ensure *no* dereferencing (and no other builtins) can run.
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/Unification_%28computer_science%29](https://en.wikipedia.org/wiki/Unification_%28computer_science%29)
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)