eyeling 1.22.12 → 1.22.14
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 +95 -48
- package/README.md +0 -8
- package/dist/browser/eyeling.browser.js +77 -27
- package/eyeling.js +73 -27
- package/lib/engine.js +47 -22
- package/lib/rules.js +25 -5
- package/package.json +1 -1
- package/test/api.test.js +28 -0
package/HANDBOOK.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## A compact Notation3 reasoner in JavaScript — a handbook
|
|
4
4
|
|
|
5
5
|
> This handbook is written for a computer science student who wants to understand Eyeling as _code_ and as a _reasoning machine_.
|
|
6
|
-
> It
|
|
6
|
+
> It is meant to be read linearly, but each chapter stands on its own.
|
|
7
7
|
|
|
8
8
|
## Contents
|
|
9
9
|
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
- [Appendix G — Eyeling and the W3C CG Notation3 Semantics](#app-g)
|
|
35
35
|
- [Appendix H — Applied Constructor-Theory and the N3 ARC examples](#app-h)
|
|
36
36
|
- [Appendix I — The Eyeling Playground](#app-i)
|
|
37
|
+
- [Appendix J — Formalism Is Fine](#app-j)
|
|
37
38
|
|
|
38
39
|
---
|
|
39
40
|
|
|
@@ -48,7 +49,7 @@ Eyeling is a small Notation3 (N3) reasoner implemented in JavaScript. Its job is
|
|
|
48
49
|
|
|
49
50
|
and compute consequences until nothing new follows.
|
|
50
51
|
|
|
51
|
-
If you
|
|
52
|
+
If you have seen Datalog or Prolog, the shape will feel familiar. Eyeling blends both:
|
|
52
53
|
|
|
53
54
|
- **Forward chaining** (like Datalog saturation) for `=>` rules.
|
|
54
55
|
- **Backward chaining** (like Prolog goal solving) for `<=` rules _and_ for built-in predicates.
|
|
@@ -139,7 +140,7 @@ If you want to follow the code in the same order Eyeling “thinks”, read:
|
|
|
139
140
|
10. `lib/cli.js` + `lib/entry.js` — command-line wiring and bundle entry exports.
|
|
140
141
|
11. `index.js` — the npm API wrapper (spawns the bundled CLI synchronously).
|
|
141
142
|
|
|
142
|
-
This is
|
|
143
|
+
This is very nearly a tiny compiler pipeline:
|
|
143
144
|
|
|
144
145
|
```
|
|
145
146
|
|
|
@@ -200,7 +201,7 @@ In addition, interned **Iri**/**Literal** terms (and generated **Blank** terms)
|
|
|
200
201
|
|
|
201
202
|
For blanks, the id is derived from the blank label (so different blank labels remain different existentials).
|
|
202
203
|
|
|
203
|
-
Terms are treated as immutable: once interned/created, the code assumes you
|
|
204
|
+
Terms are treated as immutable: once interned/created, the code assumes you will not mutate `.value` (or `.label` for blanks).
|
|
204
205
|
|
|
205
206
|
### 3.4 Prefix environment
|
|
206
207
|
|
|
@@ -246,7 +247,7 @@ The parser supports:
|
|
|
246
247
|
- keyword-ish sugar like `is ... of` and inverse arrows
|
|
247
248
|
- path operators `!` and `^` that may generate helper triples via fresh blanks
|
|
248
249
|
|
|
249
|
-
A
|
|
250
|
+
A useful detail: the parser maintains a `pendingTriples` list used when certain syntactic forms expand into helper triples (for example, some path/property-list expansions). It ensures the “surface statement” still emits all required triples even if the subject itself was syntactic sugar.
|
|
250
251
|
|
|
251
252
|
### 4.3 Parsing rules: `=>`, `<=`, and log idioms
|
|
252
253
|
|
|
@@ -287,7 +288,7 @@ Internally:
|
|
|
287
288
|
|
|
288
289
|
## Chapter 5 — Rule normalization: “compile-time” semantics (`lib/rules.js`)
|
|
289
290
|
|
|
290
|
-
Before rules hit the engine, Eyeling performs one lightweight transformation. A second “make it work” trick—deferring built-ins that
|
|
291
|
+
Before rules hit the engine, Eyeling performs one lightweight transformation. A second “make it work” trick—deferring built-ins that cannot run yet—happens later inside the goal prover.
|
|
291
292
|
|
|
292
293
|
### 5.1 Lifting blank nodes in rule bodies into variables
|
|
293
294
|
|
|
@@ -309,9 +310,35 @@ This avoids the “existential in the body” trap and matches how most rule aut
|
|
|
309
310
|
|
|
310
311
|
Blanks in the **conclusion** are _not_ lifted — they remain blanks and later become existentials (Chapter 9).
|
|
311
312
|
|
|
312
|
-
### 5.1.1 Quoted formulas
|
|
313
|
+
### 5.1.1 Quoted formulas in rule bodies: direct pattern positions vs nested data positions
|
|
313
314
|
|
|
314
|
-
There is one important
|
|
315
|
+
There is one important refinement to the “lift blanks in rule bodies” rule when a rule body mentions a quoted formula (`GraphTerm`).
|
|
316
|
+
|
|
317
|
+
Eyeling now distinguishes **direct quoted-formula positions** from **nested quoted-formula data**.
|
|
318
|
+
|
|
319
|
+
#### Direct quoted-formula positions in a premise triple
|
|
320
|
+
|
|
321
|
+
When a quoted formula appears **directly** as the subject, predicate, or object term of a premise triple, Eyeling treats blank nodes inside that quoted formula as **rule-body placeholders** and lifts them to rule variables.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
|
|
325
|
+
```n3
|
|
326
|
+
{ :A :B :C } a :Statement.
|
|
327
|
+
|
|
328
|
+
{
|
|
329
|
+
{ _:X :B :C } a :Statement.
|
|
330
|
+
} => {
|
|
331
|
+
:result :is true.
|
|
332
|
+
}.
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
This matches and derives `:result :is true.` because the direct quoted formula `{ _:X :B :C }` is being used as a **pattern-bearing term** in the premise triple.
|
|
336
|
+
|
|
337
|
+
This behavior is mainly for interoperability with engines that treat blank nodes in such direct quoted-formula premise positions as pattern placeholders.
|
|
338
|
+
|
|
339
|
+
#### Nested quoted formulas remain data
|
|
340
|
+
|
|
341
|
+
If the quoted formula is nested **inside another term** in the rule body — for example inside a list used by `log:conjunction` — Eyeling preserves the quoted formula’s own blank-node scope.
|
|
315
342
|
|
|
316
343
|
So this rule body:
|
|
317
344
|
|
|
@@ -321,24 +348,24 @@ So this rule body:
|
|
|
321
348
|
} => { ... }.
|
|
322
349
|
```
|
|
323
350
|
|
|
324
|
-
must keep the inner `[]` as a **formula-local blank node**. Eyeling
|
|
351
|
+
must keep the inner `[]` as a **formula-local blank node**. Eyeling treats it as belonging to the quoted graph, not as a rule-body variable that escapes into the surrounding rule.
|
|
325
352
|
|
|
326
|
-
That distinction matters because quoted formulas play **two different roles** in Eyeling:
|
|
353
|
+
That distinction matters because quoted formulas still play **two different roles** in Eyeling:
|
|
327
354
|
|
|
328
|
-
1. **Formula as data** — for example when constructing a formula with `log:conjunction` or storing `{ ... }`
|
|
329
|
-
2. **Formula as a query pattern** —
|
|
355
|
+
1. **Formula as data** — for example when constructing a formula with `log:conjunction` or storing `{ ... }` inside another data term. In this role, local blanks stay blanks. They print as blank nodes and participate in alpha-equivalence only within that quoted formula.
|
|
356
|
+
2. **Formula as a query pattern** — either through query-like builtins such as `log:includes`, `log:notIncludes`, `log:collectAllIn`, or `log:forAllIn`, or through a **direct quoted-formula premise position** as described above. In that role, the formula’s local blanks may be treated existentially while matching.
|
|
330
357
|
|
|
331
358
|
The practical rule is:
|
|
332
359
|
|
|
333
|
-
> **
|
|
360
|
+
> **Eyeling lifts blanks inside quoted formulas only when the quoted formula appears directly in a premise triple position. Nested quoted formulas remain scoped data unless a query-like builtin interprets them as patterns.**
|
|
334
361
|
|
|
335
|
-
This
|
|
362
|
+
This keeps `log:conjunction` and formula printing honest, while still allowing direct quoted-formula premise patterns such as `{ _:X :B :C } a :Statement.` to match interoperably.
|
|
336
363
|
|
|
337
364
|
### 5.2 Builtin deferral in forward-rule bodies
|
|
338
365
|
|
|
339
|
-
In a depth-first proof, the order of goals matters. Many built-ins only become informative once parts of the triple are **already instantiated** (for example comparisons, pattern tests, and other built-ins that
|
|
366
|
+
In a depth-first proof, the order of goals matters. Many built-ins only become informative once parts of the triple are **already instantiated** (for example comparisons, pattern tests, and other built-ins that do not normally create bindings).
|
|
340
367
|
|
|
341
|
-
If such a builtin runs while its subject/object still contain variables or blanks, it may return **no solutions** (because it
|
|
368
|
+
If such a builtin runs while its subject/object still contain variables or blanks, it may return **no solutions** (because it cannot decide yet) or only the **empty delta** (`{}`), even though it would succeed (or fail) once other goals have bound the needed values.
|
|
342
369
|
|
|
343
370
|
Eyeling supports a runtime deferral mechanism inside `proveGoals(...)`, enabled only when proving the bodies of forward rules.
|
|
344
371
|
|
|
@@ -351,9 +378,9 @@ What happens when `proveGoals(..., { deferBuiltins: true })` sees a builtin goal
|
|
|
351
378
|
- the goal list hasn’t already been rotated too many times,
|
|
352
379
|
- then Eyeling **rotates that builtin goal to the end** of the current goal list and continues with the next goal first.
|
|
353
380
|
|
|
354
|
-
A small counter (`deferCount`) caps how many rotations can happen (at most the length of the current goal list), so the prover
|
|
381
|
+
A small counter (`deferCount`) caps how many rotations can happen (at most the length of the current goal list), so the prover cannot loop forever by endlessly “trying later”.
|
|
355
382
|
|
|
356
|
-
There is one extra guard for a small whitelist of built-ins that are considered satisfiable even when both subject and object are completely unbound (see `__builtinIsSatisfiableWhenFullyUnbound`). For these, if evaluation yields no deltas and there is nothing left to bind (either it is the last goal, or deferral has already been exhausted), Eyeling treats the builtin as a vacuous success (`[{}]`) so it
|
|
383
|
+
There is one extra guard for a small whitelist of built-ins that are considered satisfiable even when both subject and object are completely unbound (see `__builtinIsSatisfiableWhenFullyUnbound`). For these, if evaluation yields no deltas and there is nothing left to bind (either it is the last goal, or deferral has already been exhausted), Eyeling treats the builtin as a vacuous success (`[{}]`) so it does not block the proof.
|
|
357
384
|
|
|
358
385
|
This is intentionally enabled for **forward-chaining rule bodies only**. Backward rules keep their normal left-to-right goal order, which can be important for termination on some programs.
|
|
359
386
|
|
|
@@ -399,7 +426,7 @@ Eyeling has ordinary structural equality (term-by-term) for most terms.
|
|
|
399
426
|
|
|
400
427
|
But **quoted formulas** (`GraphTerm`) demand something stronger. Two formulas should match even if their internal blank/variable names differ, as long as the structure is the same.
|
|
401
428
|
|
|
402
|
-
That
|
|
429
|
+
That is alpha-equivalence:
|
|
403
430
|
|
|
404
431
|
- `{ _:x :p ?y. }` should match `{ _:z :p ?w. }`
|
|
405
432
|
|
|
@@ -409,14 +436,16 @@ Important scope nuance: only blanks/variables that are local to the quoted formu
|
|
|
409
436
|
|
|
410
437
|
So `{ _:x :p :o }` obtained by substituting `?A = _:x` into `{ ?A :p :o }` must not alpha-match `{ _:b :p :o }` by renaming `_:x` to `_:b`.
|
|
411
438
|
|
|
412
|
-
|
|
439
|
+
A related operational detail matters for rule execution: alpha-equivalence is only a **binding-free shortcut** when both quoted formulas are variable-free after substitution. If unbound variables still remain inside the formulas, Eyeling must fall back to structural quoted-formula unification so shared outer rule variables can actually bind. Otherwise a premise such as `?A :has { ?S ?P ?O }` could appear to match while leaving `?S ?P ?O` unbound for later goals.
|
|
440
|
+
|
|
441
|
+
### 6.2 Groundness: “variables inside formulas do not leak”
|
|
413
442
|
|
|
414
443
|
Eyeling makes a deliberate choice about _groundness_:
|
|
415
444
|
|
|
416
445
|
- a triple is “ground” if it has no free variables in normal positions
|
|
417
446
|
- **variables inside a `GraphTerm` do not make the surrounding triple non-ground**
|
|
418
447
|
|
|
419
|
-
This is encoded in functions like `isGroundTermInGraph`. It
|
|
448
|
+
This is encoded in functions like `isGroundTermInGraph`. It is what makes it possible to assert and store triples that _mention formulas with variables_ as data.
|
|
420
449
|
|
|
421
450
|
### 6.3 Substitutions: chaining and application
|
|
422
451
|
|
|
@@ -467,8 +496,8 @@ Unification is implemented in `unifyTerm` / `unifyTriple`, with support for:
|
|
|
467
496
|
|
|
468
497
|
There are two key traits of Eyeling’s graph unification:
|
|
469
498
|
|
|
470
|
-
1. It
|
|
471
|
-
2. It
|
|
499
|
+
1. It is _set-like_: order does not matter.
|
|
500
|
+
2. It is _substitution-threaded_: choices made while matching one triple restrict the remaining matches, just like Prolog.
|
|
472
501
|
|
|
473
502
|
### 6.5 Literals: lexical vs semantic equality
|
|
474
503
|
|
|
@@ -569,7 +598,7 @@ for delta in deltas:
|
|
|
569
598
|
undoTo(mark)
|
|
570
599
|
```
|
|
571
600
|
|
|
572
|
-
**Implementation note (performance):** in the core DFS, Eyeling applies builtin (and unification) deltas into a single mutable substitution and uses a **trail** to undo bindings on backtracking. This preserves the meaning of “threading substitutions through a proof”, but avoids allocating and copying full substitution objects on every branch. Empty deltas (`{}`) are genuinely cheap: they
|
|
601
|
+
**Implementation note (performance):** in the core DFS, Eyeling applies builtin (and unification) deltas into a single mutable substitution and uses a **trail** to undo bindings on backtracking. This preserves the meaning of “threading substitutions through a proof”, but avoids allocating and copying full substitution objects on every branch. Empty deltas (`{}`) are genuinely cheap: they do not touch the trail and only incur the control-flow overhead of exploring a branch.
|
|
573
602
|
|
|
574
603
|
**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.
|
|
575
604
|
|
|
@@ -577,9 +606,9 @@ So built-ins behave like relations that can generate zero, one, or many possible
|
|
|
577
606
|
|
|
578
607
|
#### 8.3.1 Builtin deferral and “vacuous” solutions
|
|
579
608
|
|
|
580
|
-
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
|
|
609
|
+
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 cannot 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).
|
|
581
610
|
|
|
582
|
-
“
|
|
611
|
+
“Cannot make progress” includes both cases:
|
|
583
612
|
|
|
584
613
|
- the builtin returns **no solutions** (`[]`), and
|
|
585
614
|
- the builtin returns only **vacuous solutions** (`[{}]`, i.e., success with _no new bindings_) while the goal still contains unbound vars/blanks.
|
|
@@ -716,7 +745,7 @@ Eyeling handles this by replacing head blank labels with fresh blank labels of t
|
|
|
716
745
|
|
|
717
746
|
- `_:sk_0`, `_:sk_1`, …
|
|
718
747
|
|
|
719
|
-
But it does something subtle and important: it caches skolemization per (rule firing, head blank label), so that the _same_ firing instance
|
|
748
|
+
But it does something subtle and important: it caches skolemization per (rule firing, head blank label), so that the _same_ firing instance does not keep generating new blanks across outer iterations.
|
|
720
749
|
|
|
721
750
|
The “firing instance” is keyed by a deterministic string derived from the instantiated body (“firingKey”). This stabilizes the closure and prevents “existential churn.”
|
|
722
751
|
|
|
@@ -803,7 +832,7 @@ Some built-ins interpret a positive integer literal as a requested priority:
|
|
|
803
832
|
|
|
804
833
|
If a rule requests priority `N`, Eyeling delays that builtin until `scopedClosureLevel >= N`.
|
|
805
834
|
|
|
806
|
-
In practice this allows rule authors to write “
|
|
835
|
+
In practice this allows rule authors to write “do not run this scoped query until the closure is stable enough” and is what lets Eyeling iterate safely when rule-producing rules introduce new needs.
|
|
807
836
|
|
|
808
837
|
### 10.3 `log:conclusion`: local deductive closure of a formula
|
|
809
838
|
|
|
@@ -814,7 +843,7 @@ In practice this allows rule authors to write “don’t run this scoped query u
|
|
|
814
843
|
- extract rule triples inside it (`log:implies`, `log:impliedBy`)
|
|
815
844
|
- run `forwardChain` locally over those triples
|
|
816
845
|
|
|
817
|
-
- cache the result in a `WeakMap` so the same formula
|
|
846
|
+
- cache the result in a `WeakMap` so the same formula does not get recomputed
|
|
818
847
|
|
|
819
848
|
Notably, `log:impliedBy` inside the formula is treated as forward implication too for closure computation (and also indexed as backward to help proving).
|
|
820
849
|
|
|
@@ -882,7 +911,7 @@ The N3 Builtins tradition often describes builtins using “schema” annotation
|
|
|
882
911
|
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:
|
|
883
912
|
|
|
884
913
|
1. **Variables (`?X`) may be bound** by a builtin if the builtin is written to do so.
|
|
885
|
-
2. **Blank nodes (`[]` / `_:`)** are frequently treated as “
|
|
914
|
+
2. **Blank nodes (`[]` / `_:`)** are frequently treated as “do not care” placeholders. Many builtins accept a blank node in an output position and simply succeed without binding.
|
|
886
915
|
3. **Fully unbound relations are usually not enumerated.** If both sides are unbound and enumerating solutions would be infinite (or huge), a number of builtins treat that situation as “satisfiable” and succeed once without binding anything. (This is mainly to keep meta-tests and some N3 conformance cases happy.)
|
|
887
916
|
|
|
888
917
|
With that, we can tour the builtin families as Eyeling actually implements them.
|
|
@@ -1006,7 +1035,7 @@ Eyeling supports:
|
|
|
1006
1035
|
3. **DateTime minus duration**: `(dateTime durationOrSeconds) math:difference dateTime`
|
|
1007
1036
|
- Subtracts a duration from a dateTime and yields a new dateTime.
|
|
1008
1037
|
|
|
1009
|
-
If the types
|
|
1038
|
+
If the types do not fit any supported case, the builtin fails.
|
|
1010
1039
|
|
|
1011
1040
|
#### `math:quotient`
|
|
1012
1041
|
|
|
@@ -1062,7 +1091,7 @@ The **BigInt exact-integer mode** exists specifically to avoid rule-level “rep
|
|
|
1062
1091
|
|
|
1063
1092
|
#### Unary “math relations” (often invertible)
|
|
1064
1093
|
|
|
1065
|
-
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 (
|
|
1094
|
+
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 (do not enumerate).
|
|
1066
1095
|
|
|
1067
1096
|
- `math:absoluteValue`
|
|
1068
1097
|
- `math:negation`
|
|
@@ -1124,7 +1153,7 @@ Binds `?now` to the current local time as an `xsd:dateTime` literal.
|
|
|
1124
1153
|
|
|
1125
1154
|
Two subtle but important engineering choices:
|
|
1126
1155
|
|
|
1127
|
-
1. Eyeling memoizes “now” per reasoning run so that repeated uses in one run
|
|
1156
|
+
1. Eyeling memoizes “now” per reasoning run so that repeated uses in one run do not drift.
|
|
1128
1157
|
2. Eyeling supports a fixed “now” override (used for deterministic tests).
|
|
1129
1158
|
|
|
1130
1159
|
---
|
|
@@ -1253,7 +1282,7 @@ Reversible in the sense that either side may be the list:
|
|
|
1253
1282
|
- If subject is a list, object becomes its reversal.
|
|
1254
1283
|
- If object is a list, subject becomes its reversal.
|
|
1255
1284
|
|
|
1256
|
-
It does not enumerate arbitrary reversals; it
|
|
1285
|
+
It does not enumerate arbitrary reversals; it is a deterministic transform once one side is known.
|
|
1257
1286
|
|
|
1258
1287
|
#### `list:remove`
|
|
1259
1288
|
|
|
@@ -1271,7 +1300,7 @@ Succeeds iff the object cannot be unified with any element of the subject list.
|
|
|
1271
1300
|
|
|
1272
1301
|
#### `list:append`
|
|
1273
1302
|
|
|
1274
|
-
This is list concatenation, but Eyeling implements it in a
|
|
1303
|
+
This is list concatenation, but Eyeling implements it in a usefully relational way.
|
|
1275
1304
|
|
|
1276
1305
|
**Forward shape:** `( (a b) (c) (d e) ) list:append (a b c d e)`
|
|
1277
1306
|
|
|
@@ -1377,7 +1406,7 @@ These builtins reach outside the current fact set. They are synchronous by desig
|
|
|
1377
1406
|
|
|
1378
1407
|
Dereferences and parses the remote/local resource as N3/Turtle-like syntax, returning a formula.
|
|
1379
1408
|
|
|
1380
|
-
A
|
|
1409
|
+
A useful 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.
|
|
1381
1410
|
|
|
1382
1411
|
#### `log:semanticsOrError`
|
|
1383
1412
|
|
|
@@ -1499,7 +1528,7 @@ Eyeling has **two modes**:
|
|
|
1499
1528
|
|
|
1500
1529
|
2. **Priority-gated global scope**: otherwise
|
|
1501
1530
|
- Eyeling uses a _frozen snapshot_ of the current global closure.
|
|
1502
|
-
- The “priority” is read from the subject if it
|
|
1531
|
+
- The “priority” is read from the subject if it is a positive integer literal `N`.
|
|
1503
1532
|
- If the closure level is below `N`, the builtin “delays” by failing at that point in the search.
|
|
1504
1533
|
|
|
1505
1534
|
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.
|
|
@@ -1565,7 +1594,7 @@ Bidirectional conversion between IRIs and their string form:
|
|
|
1565
1594
|
|
|
1566
1595
|
- If subject is an IRI, object can be unified with a string literal of its IRI.
|
|
1567
1596
|
- 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.
|
|
1568
|
-
- Some “fully unbound /
|
|
1597
|
+
- Some “fully unbound / do not-care” combinations succeed once to avoid infinite enumeration.
|
|
1569
1598
|
|
|
1570
1599
|
### Side effects and output directives
|
|
1571
1600
|
|
|
@@ -1588,7 +1617,7 @@ As a goal, this builtin simply checks that the terms are sufficiently bound/usab
|
|
|
1588
1617
|
- When the final closure contains any `log:outputString` triples, the CLI collects all of them from the _saturated_ closure and renders those strings instead of the default N3 output.
|
|
1589
1618
|
- It sorts them deterministically by the subject “key” and concatenates the string values in that order.
|
|
1590
1619
|
|
|
1591
|
-
This is a pure test/side-effect marker (it
|
|
1620
|
+
This is a pure test/side-effect marker (it should not 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.
|
|
1592
1621
|
|
|
1593
1622
|
---
|
|
1594
1623
|
|
|
@@ -1763,7 +1792,7 @@ Dereferencing is cached by IRI-without-fragment (fragments are stripped). There
|
|
|
1763
1792
|
- parsed semantics (GraphTerm)
|
|
1764
1793
|
- semantics-or-error
|
|
1765
1794
|
|
|
1766
|
-
This is both a performance and a stability feature: repeated `log:semantics` calls in backward proofs
|
|
1795
|
+
This is both a performance and a stability feature: repeated `log:semantics` calls in backward proofs will not keep refetching.
|
|
1767
1796
|
|
|
1768
1797
|
### 12.3 HTTPS enforcement
|
|
1769
1798
|
|
|
@@ -1796,7 +1825,7 @@ When enabled, Eyeling prints a compact comment block per derived triple:
|
|
|
1796
1825
|
- the instantiated rule body that was provable
|
|
1797
1826
|
- the schematic forward rule that produced it
|
|
1798
1827
|
|
|
1799
|
-
It
|
|
1828
|
+
It is a “why this triple holds” explanation, not a globally exported proof graph.
|
|
1800
1829
|
|
|
1801
1830
|
Implementation note: the engine records lightweight `DerivedFact` objects during forward chaining, and `lib/explain.js` (via `makeExplain(...)`) is responsible for turning those objects into the human-readable proof comment blocks.
|
|
1802
1831
|
|
|
@@ -2129,7 +2158,7 @@ What Eyeling does:
|
|
|
2129
2158
|
|
|
2130
2159
|
6. The triple is ground and not already present, so it is added and (optionally) printed.
|
|
2131
2160
|
|
|
2132
|
-
That
|
|
2161
|
+
That is the whole engine in miniature: unify, compose substitutions, emit head triples.
|
|
2133
2162
|
|
|
2134
2163
|
---
|
|
2135
2164
|
|
|
@@ -2659,7 +2688,7 @@ Example fuse:
|
|
|
2659
2688
|
} => false.
|
|
2660
2689
|
```
|
|
2661
2690
|
|
|
2662
|
-
If you
|
|
2691
|
+
If you do not want “stop the world”, derive a `:Violation` fact instead, and keep going.
|
|
2663
2692
|
|
|
2664
2693
|
### 3) Make the workflow test-driven (golden closures)
|
|
2665
2694
|
|
|
@@ -2673,7 +2702,7 @@ This turns rule edits into a normal change-management loop: diffs are explicit,
|
|
|
2673
2702
|
|
|
2674
2703
|
### 4) Use proofs/traces as the input to the LLM, not the other way around
|
|
2675
2704
|
|
|
2676
|
-
If you want a natural-language explanation,
|
|
2705
|
+
If you want a natural-language explanation, do not ask the model to “explain the rules from memory”. Instead:
|
|
2677
2706
|
|
|
2678
2707
|
1. Run Eyeling with proof/trace enabled (Eyeling has explicit tracing hooks and proof-comment support in its output pipeline).
|
|
2679
2708
|
2. Give the LLM the **derived triples + proof comments** and ask it to summarize:
|
|
@@ -2706,7 +2735,7 @@ A simple structure that keeps the LLM honest:
|
|
|
2706
2735
|
- “Include at least N minimal tests as facts in a separate block/file.”
|
|
2707
2736
|
- “If something is unknown, emit a placeholder fact (`:needsFact`) rather than guessing.”
|
|
2708
2737
|
|
|
2709
|
-
The point
|
|
2738
|
+
The point is not that the LLM is “right”; it is that **Eyeling makes the result checkable**, and the artifact becomes a maintainable program rather than a one-off generation.
|
|
2710
2739
|
|
|
2711
2740
|
---
|
|
2712
2741
|
|
|
@@ -3190,8 +3219,8 @@ The following examples are especially useful if you want to see Eyeling files th
|
|
|
3190
3219
|
- [`examples/act-gravity-mediator-witness.n3`](examples/act-gravity-mediator-witness.n3) · [`examples/output/act-gravity-mediator-witness.txt`](examples/output/act-gravity-mediator-witness.txt) — applied constructor-theory witness showing that, under locality and interoperability, entanglement mediated only by gravity implies a non-classical gravitational mediator.
|
|
3191
3220
|
- ['examples/act-yeast-self-reproduction.n3'](examples/act-yeast-self-reproduction.n3) · ['examples/output/act-yeast-self-reproduction.txt'](examples/output/act-yeast-self-reproduction.txt) — applied constructor-theory example of a yeast starter culture showing replicator, vehicle, self-reproduction, heritable variation, and natural selection under no-design laws.
|
|
3192
3221
|
- ['examples/act-barley-seed-lineage.n3'](examples/act-barley-seed-lineage.n3) · ['examples/output/act-barley-seed-lineage.txt'](examples/output/act-barley-seed-lineage.txt) — applied constructor-theory ARC case showing both possible and impossible lineage tasks under no-design laws, including blocked reproduction, dormancy, and evolvability when key ingredients are missing.
|
|
3193
|
-
- ['examples/act-tunnel-junction-wake-switch.n3'](examples/act-tunnel-junction-wake-switch.n3) · ['examples/output/act-tunnel-junction-wake-switch.txt'](examples/output/act-tunnel-junction-wake-switch.txt) — applied constructor-theory ARC case comparing a tunnel-junction wake switch with a conventional PN junction via explicit can/
|
|
3194
|
-
- ['examples/act-photosynthetic-exciton-transfer.n3'](examples/act-photosynthetic-exciton-transfer.n3) · ['examples/output/act-photosynthetic-exciton-transfer.txt'](examples/output/act-photosynthetic-exciton-transfer.txt) — applied constructor-theory ARC case for quantum-assisted exciton transfer in a photosynthetic antenna, contrasting a tuned complex with a detuned one via explicit can/
|
|
3222
|
+
- ['examples/act-tunnel-junction-wake-switch.n3'](examples/act-tunnel-junction-wake-switch.n3) · ['examples/output/act-tunnel-junction-wake-switch.txt'](examples/output/act-tunnel-junction-wake-switch.txt) — applied constructor-theory ARC case comparing a tunnel-junction wake switch with a conventional PN junction via explicit can/cannot rules for tunneling, sub-threshold current, negative differential response, and low-bias switching.
|
|
3223
|
+
- ['examples/act-photosynthetic-exciton-transfer.n3'](examples/act-photosynthetic-exciton-transfer.n3) · ['examples/output/act-photosynthetic-exciton-transfer.txt'](examples/output/act-photosynthetic-exciton-transfer.txt) — applied constructor-theory ARC case for quantum-assisted exciton transfer in a photosynthetic antenna, contrasting a tuned complex with a detuned one via explicit can/cannot rules.
|
|
3195
3224
|
- ['examples/act-sensor-memory-reset.n3'](examples/act-sensor-memory-reset.n3) · ['examples/output/act-sensor-memory-reset.txt'](examples/output/act-sensor-memory-reset.txt) — applied constructor-theory ARC case showing that a sensor memory reset is possible with a work medium but not with heat alone, highlighting work/heat distinction and irreversibility.
|
|
3196
3225
|
|
|
3197
3226
|
#### Deep-classification stress tests
|
|
@@ -3336,7 +3365,7 @@ and just as naturally:
|
|
|
3336
3365
|
{ ?system :lacks ?property . } => { ?system :cannot ?task . } .
|
|
3337
3366
|
```
|
|
3338
3367
|
|
|
3339
|
-
That is already close to the “science of can and
|
|
3368
|
+
That is already close to the “science of can and cannot” idiom.
|
|
3340
3369
|
|
|
3341
3370
|
Second, N3 can keep the explanation close to the answer. The conditions, the derived `:can` / `:cannot` facts, and the final human-readable report can all live in one file.
|
|
3342
3371
|
|
|
@@ -3633,3 +3662,21 @@ In short: the playground is best thought of as a compact interactive front end f
|
|
|
3633
3662
|
The Eyeling Playground shows that N3 reasoning can be made substantially more approachable without flattening the underlying logic into a toy interface. A relatively small set of features — an editor, a URL loader, background knowledge mode, responsive execution, proof toggles, and shareable query parameters — is enough to support serious educational and exploratory work.
|
|
3634
3663
|
|
|
3635
3664
|
That is the main value of the playground. It gives Eyeling a public-facing, browser-native environment where reasoning is not hidden behind setup overhead, and where examples can move easily between author, teacher, student, and reviewer.
|
|
3665
|
+
|
|
3666
|
+
<a id="app-j"></a>
|
|
3667
|
+
|
|
3668
|
+
## Appendix J — Formalism Is Fine
|
|
3669
|
+
|
|
3670
|
+
For Eyeling, formal methods are not an obstacle to practical reasoning. They are part of what makes the system useful. A reasoner is easier to trust when its facts, rules, derivations, and limits can be stated explicitly rather than hidden in application code. That is the sense in which formalism matters here: not as ceremony, but as a way of keeping the behavior of the system inspectable.
|
|
3671
|
+
|
|
3672
|
+
Horn logic is fine because it gives a disciplined core. It does not try to express every possible form of reasoning. Instead, it offers a fragment that is small enough to implement clearly and strong enough to support a wide range of real tasks. That trade is often a good one. In a compact reasoner, expressiveness only helps when it does not destroy clarity or operational control.
|
|
3673
|
+
|
|
3674
|
+
Notation3 is fine because a logic language also needs a readable surface. Eyeling works with terms, triples, formulas, and rules, but those structures still have to be written, reviewed, debugged, and shared. N3 matters because it keeps the logic close to the page. A rule still looks like something a person can follow. A quoted formula still looks like a graph that can be inspected. That readability is part of what makes the reasoner teachable and portable.
|
|
3675
|
+
|
|
3676
|
+
Executable specification is fine because there is real value in keeping semantics and implementation close together. When a specification can be run, it becomes easier to test the intended behavior on concrete inputs, compare outcomes across examples, and find the points where an abstract account is still too vague. Execution does not replace semantics, but it is often the best way to expose whether the semantics is precise enough to guide an implementation.
|
|
3677
|
+
|
|
3678
|
+
Herbrand semantics is fine because it gives symbolic reasoning a concrete semantic basis. Instead of beginning with an opaque external domain, it begins with the symbolic constructions themselves and asks what follows from them under the rules. That is a natural fit for Eyeling. The engine reasons over terms, substitutions, triples, formulas, and proof states. Herbrand-style semantics therefore does not feel like an imported philosophical story. It describes the level at which the system actually works.
|
|
3679
|
+
|
|
3680
|
+
Gödel incompleteness is fine because the limits of formal systems are not a refutation of formal reasoning. They are part of its shape. Once a system becomes expressive enough, one should expect structural limits on what it can prove about itself. That does not make formal methods less serious. It shows that their boundaries are principled rather than accidental. For a handbook like this one, that is the right lesson: formal systems are valuable not because they say everything, but because they say some things clearly, explicitly, and in a form that can be checked.
|
|
3681
|
+
|
|
3682
|
+
Taken together, these positions support a straightforward attitude toward Eyeling. Horn logic is fine. Notation3 is fine. Executable specification is fine. Herbrand semantics is fine. Gödel incompleteness is fine. None of these commitments make the reasoner narrower in a harmful sense. They make it clearer, easier to inspect, and easier to trust. For this project, that is enough.
|
package/README.md
CHANGED
|
@@ -17,11 +17,3 @@ echo '@prefix : <http://example.org/> .
|
|
|
17
17
|
- **Handbook:** [eyereasoner.github.io/eyeling/HANDBOOK](https://eyereasoner.github.io/eyeling/HANDBOOK)
|
|
18
18
|
- **Playground:** [eyereasoner.github.io/eyeling/demo](https://eyereasoner.github.io/eyeling/demo)
|
|
19
19
|
- **Conformance report:** [codeberg.org/phochste/notation3tests/.../report.md](https://codeberg.org/phochste/notation3tests/src/branch/main/reports/report.md)
|
|
20
|
-
|
|
21
|
-
## Playground URL parameters
|
|
22
|
-
|
|
23
|
-
- `edit` sets the editor content.
|
|
24
|
-
- `url` fills the URL field.
|
|
25
|
-
- `url` auto-loads when `loadbg=true`, or when no explicit `edit` was provided.
|
|
26
|
-
- `proofcomments` and `httpsderef` initialize the two checkboxes.
|
|
27
|
-
- Existing hash-based links are still read as a fallback, but new state updates write query parameters.
|
|
@@ -7625,6 +7625,22 @@
|
|
|
7625
7625
|
return s2;
|
|
7626
7626
|
}
|
|
7627
7627
|
|
|
7628
|
+
function graphTriplesContainVars(triples) {
|
|
7629
|
+
function termHasVar(t) {
|
|
7630
|
+
if (t instanceof Var) return true;
|
|
7631
|
+
if (t instanceof ListTerm) return t.elems.some(termHasVar);
|
|
7632
|
+
if (t instanceof OpenListTerm) return t.prefix.some(termHasVar) || true;
|
|
7633
|
+
if (t instanceof GraphTerm)
|
|
7634
|
+
return t.triples.some((tr) => termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o));
|
|
7635
|
+
return false;
|
|
7636
|
+
}
|
|
7637
|
+
|
|
7638
|
+
for (const tr of triples) {
|
|
7639
|
+
if (termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o)) return true;
|
|
7640
|
+
}
|
|
7641
|
+
return false;
|
|
7642
|
+
}
|
|
7643
|
+
|
|
7628
7644
|
function unifyGraphTriples(xs, ys, subst) {
|
|
7629
7645
|
if (xs.length !== ys.length) return null;
|
|
7630
7646
|
|
|
@@ -7786,17 +7802,22 @@
|
|
|
7786
7802
|
|
|
7787
7803
|
// Graphs
|
|
7788
7804
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7805
|
+
// Only use alpha-equivalence as a binding-free fast path when both quoted
|
|
7806
|
+
// formulas are variable-free after substitution. If unbound variables remain,
|
|
7807
|
+
// they may be shared with the outer rule and must unify/bind structurally.
|
|
7808
|
+
if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
|
|
7809
|
+
const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
|
|
7810
|
+
const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
|
|
7811
|
+
if (
|
|
7812
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
7813
|
+
protectedVarsA: protectedNamesA.protectedVars,
|
|
7814
|
+
protectedVarsB: protectedNamesB.protectedVars,
|
|
7815
|
+
protectedBlanksA: protectedNamesA.protectedBlanks,
|
|
7816
|
+
protectedBlanksB: protectedNamesB.protectedBlanks,
|
|
7817
|
+
})
|
|
7818
|
+
) {
|
|
7819
|
+
return subst;
|
|
7820
|
+
}
|
|
7800
7821
|
}
|
|
7801
7822
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
7802
7823
|
}
|
|
@@ -8177,17 +8198,22 @@
|
|
|
8177
8198
|
|
|
8178
8199
|
// Graphs
|
|
8179
8200
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8201
|
+
// Only use alpha-equivalence as a binding-free fast path when both quoted
|
|
8202
|
+
// formulas are variable-free after substitution. If unbound variables remain,
|
|
8203
|
+
// they may be shared with the outer rule and must unify/bind structurally.
|
|
8204
|
+
if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
|
|
8205
|
+
const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
|
|
8206
|
+
const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
|
|
8207
|
+
if (
|
|
8208
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
8209
|
+
protectedVarsA: protectedNamesA.protectedVars,
|
|
8210
|
+
protectedVarsB: protectedNamesB.protectedVars,
|
|
8211
|
+
protectedBlanksA: protectedNamesA.protectedBlanks,
|
|
8212
|
+
protectedBlanksB: protectedNamesB.protectedBlanks,
|
|
8213
|
+
})
|
|
8214
|
+
) {
|
|
8215
|
+
return true;
|
|
8216
|
+
}
|
|
8191
8217
|
}
|
|
8192
8218
|
const merged = unifyGraphTriples(a.triples, b.triples, substMut);
|
|
8193
8219
|
if (merged === null) return false;
|
|
@@ -13039,15 +13065,39 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
|
|
|
13039
13065
|
return t;
|
|
13040
13066
|
}
|
|
13041
13067
|
|
|
13042
|
-
function
|
|
13068
|
+
function convertQuotedPatternTerm(t) {
|
|
13043
13069
|
if (t instanceof Blank) return blankToVar(t.label);
|
|
13044
|
-
if (t instanceof ListTerm) return new ListTerm(t.elems.map(
|
|
13045
|
-
if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(
|
|
13046
|
-
if (t instanceof GraphTerm)
|
|
13070
|
+
if (t instanceof ListTerm) return new ListTerm(t.elems.map(convertQuotedPatternTerm));
|
|
13071
|
+
if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertQuotedPatternTerm), t.tailVar);
|
|
13072
|
+
if (t instanceof GraphTerm) {
|
|
13073
|
+
const triples = t.triples.map(
|
|
13074
|
+
(tr) =>
|
|
13075
|
+
new Triple(
|
|
13076
|
+
convertQuotedPatternTerm(tr.s),
|
|
13077
|
+
convertQuotedPatternTerm(tr.p),
|
|
13078
|
+
convertQuotedPatternTerm(tr.o),
|
|
13079
|
+
),
|
|
13080
|
+
);
|
|
13081
|
+
return copyQuotedGraphMetadata(t, new GraphTerm(triples));
|
|
13082
|
+
}
|
|
13047
13083
|
return t;
|
|
13048
13084
|
}
|
|
13049
13085
|
|
|
13050
|
-
|
|
13086
|
+
function convertTerm(t, allowDirectQuotedPattern = false) {
|
|
13087
|
+
if (t instanceof Blank) return blankToVar(t.label);
|
|
13088
|
+
if (t instanceof ListTerm) return new ListTerm(t.elems.map((e) => convertTerm(e, false)));
|
|
13089
|
+
if (t instanceof OpenListTerm)
|
|
13090
|
+
return new OpenListTerm(
|
|
13091
|
+
t.prefix.map((e) => convertTerm(e, false)),
|
|
13092
|
+
t.tailVar,
|
|
13093
|
+
);
|
|
13094
|
+
if (t instanceof GraphTerm) return allowDirectQuotedPattern ? convertQuotedPatternTerm(t) : copyQuotedTerm(t);
|
|
13095
|
+
return t;
|
|
13096
|
+
}
|
|
13097
|
+
|
|
13098
|
+
const newPremise = premise.map(
|
|
13099
|
+
(tr) => new Triple(convertTerm(tr.s, true), convertTerm(tr.p, true), convertTerm(tr.o, true)),
|
|
13100
|
+
);
|
|
13051
13101
|
return [newPremise, conclusion];
|
|
13052
13102
|
}
|
|
13053
13103
|
|
package/eyeling.js
CHANGED
|
@@ -7605,6 +7605,22 @@ function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
|
7605
7605
|
return s2;
|
|
7606
7606
|
}
|
|
7607
7607
|
|
|
7608
|
+
|
|
7609
|
+
function graphTriplesContainVars(triples) {
|
|
7610
|
+
function termHasVar(t) {
|
|
7611
|
+
if (t instanceof Var) return true;
|
|
7612
|
+
if (t instanceof ListTerm) return t.elems.some(termHasVar);
|
|
7613
|
+
if (t instanceof OpenListTerm) return t.prefix.some(termHasVar) || true;
|
|
7614
|
+
if (t instanceof GraphTerm) return t.triples.some((tr) => termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o));
|
|
7615
|
+
return false;
|
|
7616
|
+
}
|
|
7617
|
+
|
|
7618
|
+
for (const tr of triples) {
|
|
7619
|
+
if (termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o)) return true;
|
|
7620
|
+
}
|
|
7621
|
+
return false;
|
|
7622
|
+
}
|
|
7623
|
+
|
|
7608
7624
|
function unifyGraphTriples(xs, ys, subst) {
|
|
7609
7625
|
if (xs.length !== ys.length) return null;
|
|
7610
7626
|
|
|
@@ -7766,17 +7782,22 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
7766
7782
|
|
|
7767
7783
|
// Graphs
|
|
7768
7784
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7785
|
+
// Only use alpha-equivalence as a binding-free fast path when both quoted
|
|
7786
|
+
// formulas are variable-free after substitution. If unbound variables remain,
|
|
7787
|
+
// they may be shared with the outer rule and must unify/bind structurally.
|
|
7788
|
+
if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
|
|
7789
|
+
const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
|
|
7790
|
+
const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
|
|
7791
|
+
if (
|
|
7792
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
7793
|
+
protectedVarsA: protectedNamesA.protectedVars,
|
|
7794
|
+
protectedVarsB: protectedNamesB.protectedVars,
|
|
7795
|
+
protectedBlanksA: protectedNamesA.protectedBlanks,
|
|
7796
|
+
protectedBlanksB: protectedNamesB.protectedBlanks,
|
|
7797
|
+
})
|
|
7798
|
+
) {
|
|
7799
|
+
return subst;
|
|
7800
|
+
}
|
|
7780
7801
|
}
|
|
7781
7802
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
7782
7803
|
}
|
|
@@ -8157,17 +8178,22 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
8157
8178
|
|
|
8158
8179
|
// Graphs
|
|
8159
8180
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8181
|
+
// Only use alpha-equivalence as a binding-free fast path when both quoted
|
|
8182
|
+
// formulas are variable-free after substitution. If unbound variables remain,
|
|
8183
|
+
// they may be shared with the outer rule and must unify/bind structurally.
|
|
8184
|
+
if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
|
|
8185
|
+
const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
|
|
8186
|
+
const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
|
|
8187
|
+
if (
|
|
8188
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
8189
|
+
protectedVarsA: protectedNamesA.protectedVars,
|
|
8190
|
+
protectedVarsB: protectedNamesB.protectedVars,
|
|
8191
|
+
protectedBlanksA: protectedNamesA.protectedBlanks,
|
|
8192
|
+
protectedBlanksB: protectedNamesB.protectedBlanks,
|
|
8193
|
+
})
|
|
8194
|
+
) {
|
|
8195
|
+
return true;
|
|
8196
|
+
}
|
|
8171
8197
|
}
|
|
8172
8198
|
const merged = unifyGraphTriples(a.triples, b.triples, substMut);
|
|
8173
8199
|
if (merged === null) return false;
|
|
@@ -12998,15 +13024,35 @@ function liftBlankRuleVars(premise, conclusion) {
|
|
|
12998
13024
|
return t;
|
|
12999
13025
|
}
|
|
13000
13026
|
|
|
13001
|
-
function
|
|
13027
|
+
function convertQuotedPatternTerm(t) {
|
|
13002
13028
|
if (t instanceof Blank) return blankToVar(t.label);
|
|
13003
|
-
if (t instanceof ListTerm) return new ListTerm(t.elems.map(
|
|
13004
|
-
if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(
|
|
13005
|
-
if (t instanceof GraphTerm)
|
|
13029
|
+
if (t instanceof ListTerm) return new ListTerm(t.elems.map(convertQuotedPatternTerm));
|
|
13030
|
+
if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertQuotedPatternTerm), t.tailVar);
|
|
13031
|
+
if (t instanceof GraphTerm) {
|
|
13032
|
+
const triples = t.triples.map(
|
|
13033
|
+
(tr) =>
|
|
13034
|
+
new Triple(convertQuotedPatternTerm(tr.s), convertQuotedPatternTerm(tr.p), convertQuotedPatternTerm(tr.o)),
|
|
13035
|
+
);
|
|
13036
|
+
return copyQuotedGraphMetadata(t, new GraphTerm(triples));
|
|
13037
|
+
}
|
|
13006
13038
|
return t;
|
|
13007
13039
|
}
|
|
13008
13040
|
|
|
13009
|
-
|
|
13041
|
+
function convertTerm(t, allowDirectQuotedPattern = false) {
|
|
13042
|
+
if (t instanceof Blank) return blankToVar(t.label);
|
|
13043
|
+
if (t instanceof ListTerm) return new ListTerm(t.elems.map((e) => convertTerm(e, false)));
|
|
13044
|
+
if (t instanceof OpenListTerm)
|
|
13045
|
+
return new OpenListTerm(
|
|
13046
|
+
t.prefix.map((e) => convertTerm(e, false)),
|
|
13047
|
+
t.tailVar,
|
|
13048
|
+
);
|
|
13049
|
+
if (t instanceof GraphTerm) return allowDirectQuotedPattern ? convertQuotedPatternTerm(t) : copyQuotedTerm(t);
|
|
13050
|
+
return t;
|
|
13051
|
+
}
|
|
13052
|
+
|
|
13053
|
+
const newPremise = premise.map(
|
|
13054
|
+
(tr) => new Triple(convertTerm(tr.s, true), convertTerm(tr.p, true), convertTerm(tr.o, true)),
|
|
13055
|
+
);
|
|
13010
13056
|
return [newPremise, conclusion];
|
|
13011
13057
|
}
|
|
13012
13058
|
|
package/lib/engine.js
CHANGED
|
@@ -1795,6 +1795,21 @@ function unifyOpenWithList(prefix, tailv, ys, subst) {
|
|
|
1795
1795
|
return s2;
|
|
1796
1796
|
}
|
|
1797
1797
|
|
|
1798
|
+
function graphTriplesContainVars(triples) {
|
|
1799
|
+
function termHasVar(t) {
|
|
1800
|
+
if (t instanceof Var) return true;
|
|
1801
|
+
if (t instanceof ListTerm) return t.elems.some(termHasVar);
|
|
1802
|
+
if (t instanceof OpenListTerm) return t.prefix.some(termHasVar) || true;
|
|
1803
|
+
if (t instanceof GraphTerm) return t.triples.some((tr) => termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o));
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
for (const tr of triples) {
|
|
1808
|
+
if (termHasVar(tr.s) || termHasVar(tr.p) || termHasVar(tr.o)) return true;
|
|
1809
|
+
}
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1798
1813
|
function unifyGraphTriples(xs, ys, subst) {
|
|
1799
1814
|
if (xs.length !== ys.length) return null;
|
|
1800
1815
|
|
|
@@ -1956,17 +1971,22 @@ function unifyTermWithOptions(a, b, subst, opts) {
|
|
|
1956
1971
|
|
|
1957
1972
|
// Graphs
|
|
1958
1973
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1974
|
+
// Only use alpha-equivalence as a binding-free fast path when both quoted
|
|
1975
|
+
// formulas are variable-free after substitution. If unbound variables remain,
|
|
1976
|
+
// they may be shared with the outer rule and must unify/bind structurally.
|
|
1977
|
+
if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
|
|
1978
|
+
const protectedNamesA = collectProtectedNamesForTerm(aRaw, subst);
|
|
1979
|
+
const protectedNamesB = collectProtectedNamesForTerm(bRaw, subst);
|
|
1980
|
+
if (
|
|
1981
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
1982
|
+
protectedVarsA: protectedNamesA.protectedVars,
|
|
1983
|
+
protectedVarsB: protectedNamesB.protectedVars,
|
|
1984
|
+
protectedBlanksA: protectedNamesA.protectedBlanks,
|
|
1985
|
+
protectedBlanksB: protectedNamesB.protectedBlanks,
|
|
1986
|
+
})
|
|
1987
|
+
) {
|
|
1988
|
+
return subst;
|
|
1989
|
+
}
|
|
1970
1990
|
}
|
|
1971
1991
|
return unifyGraphTriples(a.triples, b.triples, subst);
|
|
1972
1992
|
}
|
|
@@ -2347,17 +2367,22 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2347
2367
|
|
|
2348
2368
|
// Graphs
|
|
2349
2369
|
if (a instanceof GraphTerm && b instanceof GraphTerm) {
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2370
|
+
// Only use alpha-equivalence as a binding-free fast path when both quoted
|
|
2371
|
+
// formulas are variable-free after substitution. If unbound variables remain,
|
|
2372
|
+
// they may be shared with the outer rule and must unify/bind structurally.
|
|
2373
|
+
if (!graphTriplesContainVars(a.triples) && !graphTriplesContainVars(b.triples)) {
|
|
2374
|
+
const protectedNamesA = collectProtectedNamesForTerm(aRaw, substMut);
|
|
2375
|
+
const protectedNamesB = collectProtectedNamesForTerm(bRaw, substMut);
|
|
2376
|
+
if (
|
|
2377
|
+
alphaEqGraphTriples(a.triples, b.triples, {
|
|
2378
|
+
protectedVarsA: protectedNamesA.protectedVars,
|
|
2379
|
+
protectedVarsB: protectedNamesB.protectedVars,
|
|
2380
|
+
protectedBlanksA: protectedNamesA.protectedBlanks,
|
|
2381
|
+
protectedBlanksB: protectedNamesB.protectedBlanks,
|
|
2382
|
+
})
|
|
2383
|
+
) {
|
|
2384
|
+
return true;
|
|
2385
|
+
}
|
|
2361
2386
|
}
|
|
2362
2387
|
const merged = unifyGraphTriples(a.triples, b.triples, substMut);
|
|
2363
2388
|
if (merged === null) return false;
|
package/lib/rules.js
CHANGED
|
@@ -40,15 +40,35 @@ function liftBlankRuleVars(premise, conclusion) {
|
|
|
40
40
|
return t;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function
|
|
43
|
+
function convertQuotedPatternTerm(t) {
|
|
44
44
|
if (t instanceof Blank) return blankToVar(t.label);
|
|
45
|
-
if (t instanceof ListTerm) return new ListTerm(t.elems.map(
|
|
46
|
-
if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(
|
|
47
|
-
if (t instanceof GraphTerm)
|
|
45
|
+
if (t instanceof ListTerm) return new ListTerm(t.elems.map(convertQuotedPatternTerm));
|
|
46
|
+
if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertQuotedPatternTerm), t.tailVar);
|
|
47
|
+
if (t instanceof GraphTerm) {
|
|
48
|
+
const triples = t.triples.map(
|
|
49
|
+
(tr) =>
|
|
50
|
+
new Triple(convertQuotedPatternTerm(tr.s), convertQuotedPatternTerm(tr.p), convertQuotedPatternTerm(tr.o)),
|
|
51
|
+
);
|
|
52
|
+
return copyQuotedGraphMetadata(t, new GraphTerm(triples));
|
|
53
|
+
}
|
|
54
|
+
return t;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function convertTerm(t, allowDirectQuotedPattern = false) {
|
|
58
|
+
if (t instanceof Blank) return blankToVar(t.label);
|
|
59
|
+
if (t instanceof ListTerm) return new ListTerm(t.elems.map((e) => convertTerm(e, false)));
|
|
60
|
+
if (t instanceof OpenListTerm)
|
|
61
|
+
return new OpenListTerm(
|
|
62
|
+
t.prefix.map((e) => convertTerm(e, false)),
|
|
63
|
+
t.tailVar,
|
|
64
|
+
);
|
|
65
|
+
if (t instanceof GraphTerm) return allowDirectQuotedPattern ? convertQuotedPatternTerm(t) : copyQuotedTerm(t);
|
|
48
66
|
return t;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
|
-
const newPremise = premise.map(
|
|
69
|
+
const newPremise = premise.map(
|
|
70
|
+
(tr) => new Triple(convertTerm(tr.s, true), convertTerm(tr.p, true), convertTerm(tr.o, true)),
|
|
71
|
+
);
|
|
52
72
|
return [newPremise, conclusion];
|
|
53
73
|
}
|
|
54
74
|
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -2278,6 +2278,34 @@ _:b a ex:Person ; ex:name "B" .
|
|
|
2278
2278
|
{
|
|
2279
2279
|
:test :is true .
|
|
2280
2280
|
}.
|
|
2281
|
+
`,
|
|
2282
|
+
expect: [/^:test\s+:is\s+true\s*\./m],
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
name: 'regression: quoted-formula backward-rule heads must bind shared variables for later goals',
|
|
2286
|
+
opt: { proofComments: false },
|
|
2287
|
+
input: `@prefix : <http://example.org/> .
|
|
2288
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
2289
|
+
|
|
2290
|
+
{ :a :b :c } a :Statement .
|
|
2291
|
+
|
|
2292
|
+
{ ?A :has { ?S ?P ?O } }
|
|
2293
|
+
<=
|
|
2294
|
+
{
|
|
2295
|
+
?A log:includes { ?S ?P ?O }.
|
|
2296
|
+
}.
|
|
2297
|
+
|
|
2298
|
+
{
|
|
2299
|
+
?A a :Statement .
|
|
2300
|
+
?A :has { ?S ?P ?O }.
|
|
2301
|
+
?S log:rawType log:Other.
|
|
2302
|
+
?P log:rawType log:Other.
|
|
2303
|
+
?O log:rawType log:Other.
|
|
2304
|
+
}
|
|
2305
|
+
=>
|
|
2306
|
+
{
|
|
2307
|
+
:test :is true .
|
|
2308
|
+
}.
|
|
2281
2309
|
`,
|
|
2282
2310
|
expect: [/^:test\s+:is\s+true\s*\./m],
|
|
2283
2311
|
},
|