eyeling 1.11.0 → 1.11.2
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 +4 -4
- package/eyeling-builtins.ttl +23 -27
- package/eyeling.js +53 -87
- package/lib/engine.js +49 -8
- package/lib/parser.js +4 -3
- package/lib/rules.js +0 -76
- package/package.json +1 -1
package/HANDBOOK.md
CHANGED
|
@@ -769,7 +769,7 @@ Eyeling also accepts an older cwm-ish variant where the **subject is a 2-element
|
|
|
769
769
|
* `NaN` is treated as **not equal to anything**, including itself, for `math:equalTo`.
|
|
770
770
|
* Comparisons involving non-parsable values simply fail.
|
|
771
771
|
|
|
772
|
-
|
|
772
|
+
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.
|
|
773
773
|
|
|
774
774
|
---
|
|
775
775
|
|
|
@@ -1078,7 +1078,7 @@ Important constraint: the item to remove must be **ground** (fully known) before
|
|
|
1078
1078
|
**Shape:**
|
|
1079
1079
|
`(a b c) list:notMember x`
|
|
1080
1080
|
|
|
1081
|
-
Succeeds iff the object cannot be unified with any element of the subject list.
|
|
1081
|
+
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.
|
|
1082
1082
|
|
|
1083
1083
|
#### `list:append`
|
|
1084
1084
|
|
|
@@ -1310,7 +1310,7 @@ This is essentially a list-producing “findall”.
|
|
|
1310
1310
|
|
|
1311
1311
|
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.
|
|
1312
1312
|
|
|
1313
|
-
|
|
1313
|
+
As a pure test (no returned bindings), this typically works best once its inputs are bound; in forward rules Eyeling may defer it if it is reached too early.
|
|
1314
1314
|
|
|
1315
1315
|
### Skolemization and URI casting
|
|
1316
1316
|
|
|
@@ -1350,7 +1350,7 @@ As a goal, this builtin simply checks that the terms are sufficiently bound/usab
|
|
|
1350
1350
|
* When you run Eyeling with `--strings` / `-r`, the CLI collects all `log:outputString` triples from the *saturated* closure.
|
|
1351
1351
|
* It sorts them deterministically by the subject “key” and concatenates the string values in that order.
|
|
1352
1352
|
|
|
1353
|
-
This is
|
|
1353
|
+
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.
|
|
1354
1354
|
|
|
1355
1355
|
---
|
|
1356
1356
|
|
package/eyeling-builtins.ttl
CHANGED
|
@@ -17,15 +17,13 @@
|
|
|
17
17
|
#
|
|
18
18
|
# Notes:
|
|
19
19
|
# - ex:kind is intentionally coarse:
|
|
20
|
-
# ex:Test = succeeds/fails
|
|
20
|
+
# ex:Test = succeeds/fails (typically no bindings)
|
|
21
21
|
# ex:Function = computes an output and may bind variables
|
|
22
22
|
# ex:Relation = unification-based, may bind variables
|
|
23
23
|
# ex:Generator = may yield multiple solutions
|
|
24
24
|
# ex:IO = dereferences/parses external content (Node or browser)
|
|
25
25
|
# ex:Meta = formula/type/meta-level operations
|
|
26
26
|
# ex:SideEffect= produces output; does not bind variables
|
|
27
|
-
# - ex:isConstraint corresponds to isConstraintBuiltin(tr) in eyeling.js, with
|
|
28
|
-
# one suggested addition noted in the comments below.
|
|
29
27
|
# ------------------------------------------------------------------------------
|
|
30
28
|
|
|
31
29
|
ex:EyelingBuiltinCatalog a ex:Catalog ;
|
|
@@ -39,8 +37,6 @@ ex:IO a ex:Kind .
|
|
|
39
37
|
ex:Meta a ex:Kind .
|
|
40
38
|
ex:SideEffect a ex:Kind .
|
|
41
39
|
|
|
42
|
-
ex:isConstraint a rdf:Property ;
|
|
43
|
-
rdfs:comment "True for builtins classified as constraint/test builtins by isConstraintBuiltin(tr) (premise reordering helper)." .
|
|
44
40
|
ex:kind a rdf:Property .
|
|
45
41
|
ex:aliasOf a rdf:Property .
|
|
46
42
|
|
|
@@ -60,22 +56,22 @@ crypto:sha512 a ex:Builtin ; ex:kind ex:Function ;
|
|
|
60
56
|
|
|
61
57
|
# --- math: comparisons (constraint/test) -----------------------------
|
|
62
58
|
|
|
63
|
-
math:equalTo a ex:Builtin ; ex:kind ex:Test
|
|
59
|
+
math:equalTo a ex:Builtin ; ex:kind ex:Test;
|
|
64
60
|
rdfs:comment "Numeric comparison (=). No bindings; succeeds iff subject and object are numerically equal (supports XSD numeric literals, including special float/double lexicals)." .
|
|
65
61
|
|
|
66
|
-
math:notEqualTo a ex:Builtin ; ex:kind ex:Test
|
|
62
|
+
math:notEqualTo a ex:Builtin ; ex:kind ex:Test;
|
|
67
63
|
rdfs:comment "Numeric comparison (!=). No bindings; succeeds iff subject and object are numerically different." .
|
|
68
64
|
|
|
69
|
-
math:greaterThan a ex:Builtin ; ex:kind ex:Test
|
|
65
|
+
math:greaterThan a ex:Builtin ; ex:kind ex:Test;
|
|
70
66
|
rdfs:comment "Numeric comparison (>). No bindings." .
|
|
71
67
|
|
|
72
|
-
math:lessThan a ex:Builtin ; ex:kind ex:Test
|
|
68
|
+
math:lessThan a ex:Builtin ; ex:kind ex:Test;
|
|
73
69
|
rdfs:comment "Numeric comparison (<). No bindings." .
|
|
74
70
|
|
|
75
|
-
math:notLessThan a ex:Builtin ; ex:kind ex:Test
|
|
71
|
+
math:notLessThan a ex:Builtin ; ex:kind ex:Test;
|
|
76
72
|
rdfs:comment "Numeric comparison (>=). No bindings." .
|
|
77
73
|
|
|
78
|
-
math:notGreaterThan a ex:Builtin ; ex:kind ex:Test
|
|
74
|
+
math:notGreaterThan a ex:Builtin ; ex:kind ex:Test;
|
|
79
75
|
rdfs:comment "Numeric comparison (<=). No bindings." .
|
|
80
76
|
|
|
81
77
|
# --- math: arithmetic / numeric functions ----------------------------
|
|
@@ -204,7 +200,7 @@ list:in a ex:Builtin ; ex:kind ex:Generator ;
|
|
|
204
200
|
list:length a ex:Builtin ; ex:kind ex:Function ;
|
|
205
201
|
rdfs:comment "Length of a list as an integer token. Strict when object is ground (no integer<->decimal equality)." .
|
|
206
202
|
|
|
207
|
-
list:notMember a ex:Builtin ; ex:kind ex:Test
|
|
203
|
+
list:notMember a ex:Builtin ; ex:kind ex:Test;
|
|
208
204
|
rdfs:comment "Constraint/test: succeeds iff object does not unify with any element of the subject list. No new bindings." .
|
|
209
205
|
|
|
210
206
|
list:reverse a ex:Builtin ; ex:kind ex:Function ;
|
|
@@ -224,7 +220,7 @@ list:firstRest a ex:Builtin ; ex:kind ex:Function ;
|
|
|
224
220
|
log:equalTo a ex:Builtin ; ex:kind ex:Relation ;
|
|
225
221
|
rdfs:comment "Unification: succeeds iff subject and object unify; may bind variables." .
|
|
226
222
|
|
|
227
|
-
log:notEqualTo a ex:Builtin ; ex:kind ex:Test
|
|
223
|
+
log:notEqualTo a ex:Builtin ; ex:kind ex:Test;
|
|
228
224
|
rdfs:comment "Constraint/test: succeeds iff subject and object do NOT unify (implemented as: if unification succeeds, builtin fails). No new bindings returned." .
|
|
229
225
|
|
|
230
226
|
log:conjunction a ex:Builtin ; ex:kind ex:Meta ;
|
|
@@ -263,13 +259,13 @@ log:impliedBy a ex:Builtin ; ex:kind ex:Relation ;
|
|
|
263
259
|
log:includes a ex:Builtin ; ex:kind ex:Generator ;
|
|
264
260
|
rdfs:comment "Proves the object formula in the (possibly scoped) facts/rules; returns the set of proof substitutions (may bind variables)." .
|
|
265
261
|
|
|
266
|
-
log:notIncludes a ex:Builtin ; ex:kind ex:Test
|
|
262
|
+
log:notIncludes a ex:Builtin ; ex:kind ex:Test;
|
|
267
263
|
rdfs:comment "Constraint/test: succeeds iff proving the object formula yields no solutions. No new bindings." .
|
|
268
264
|
|
|
269
265
|
log:collectAllIn a ex:Builtin ; ex:kind ex:Function ;
|
|
270
266
|
rdfs:comment "Scoped collector: given (valueTemplate whereClause outList) and a scope formula, collects all solutions of whereClause and binds/unifies outList with the list of instantiated valueTemplate results." .
|
|
271
267
|
|
|
272
|
-
log:forAllIn a ex:Builtin ; ex:kind ex:Test
|
|
268
|
+
log:forAllIn a ex:Builtin ; ex:kind ex:Test;
|
|
273
269
|
rdfs:comment "Constraint/test: for all solutions of whereClause, thenClause must have at least one solution in the same scope. No new bindings." .
|
|
274
270
|
|
|
275
271
|
log:skolem a ex:Builtin ; ex:kind ex:Function ;
|
|
@@ -289,40 +285,40 @@ log:outputString a ex:Builtin ; ex:kind ex:SideEffect ;
|
|
|
289
285
|
string:concatenation a ex:Builtin ; ex:kind ex:Function ;
|
|
290
286
|
rdfs:comment "Concatenates a list of string-ish terms; binds/unifies object with the resulting string literal." .
|
|
291
287
|
|
|
292
|
-
string:contains a ex:Builtin ; ex:kind ex:Test
|
|
288
|
+
string:contains a ex:Builtin ; ex:kind ex:Test;
|
|
293
289
|
rdfs:comment "Constraint/test: subject contains object (substring). No bindings." .
|
|
294
290
|
|
|
295
|
-
string:containsIgnoringCase a ex:Builtin ; ex:kind ex:Test
|
|
291
|
+
string:containsIgnoringCase a ex:Builtin ; ex:kind ex:Test;
|
|
296
292
|
rdfs:comment "Constraint/test: case-insensitive contains. No bindings." .
|
|
297
293
|
|
|
298
|
-
string:endsWith a ex:Builtin ; ex:kind ex:Test
|
|
294
|
+
string:endsWith a ex:Builtin ; ex:kind ex:Test;
|
|
299
295
|
rdfs:comment "Constraint/test: subject ends with object. No bindings." .
|
|
300
296
|
|
|
301
|
-
string:startsWith a ex:Builtin ; ex:kind ex:Test
|
|
297
|
+
string:startsWith a ex:Builtin ; ex:kind ex:Test;
|
|
302
298
|
rdfs:comment "Constraint/test: subject starts with object. No bindings." .
|
|
303
299
|
|
|
304
|
-
string:equalIgnoringCase a ex:Builtin ; ex:kind ex:Test
|
|
300
|
+
string:equalIgnoringCase a ex:Builtin ; ex:kind ex:Test;
|
|
305
301
|
rdfs:comment "Constraint/test: case-insensitive equality. No bindings." .
|
|
306
302
|
|
|
307
|
-
string:notEqualIgnoringCase a ex:Builtin ; ex:kind ex:Test
|
|
303
|
+
string:notEqualIgnoringCase a ex:Builtin ; ex:kind ex:Test;
|
|
308
304
|
rdfs:comment "Constraint/test: case-insensitive inequality. No bindings." .
|
|
309
305
|
|
|
310
|
-
string:greaterThan a ex:Builtin ; ex:kind ex:Test
|
|
306
|
+
string:greaterThan a ex:Builtin ; ex:kind ex:Test;
|
|
311
307
|
rdfs:comment "Constraint/test: Unicode codepoint order (>) on decoded strings. No bindings." .
|
|
312
308
|
|
|
313
|
-
string:lessThan a ex:Builtin ; ex:kind ex:Test
|
|
309
|
+
string:lessThan a ex:Builtin ; ex:kind ex:Test;
|
|
314
310
|
rdfs:comment "Constraint/test: Unicode codepoint order (<) on decoded strings. No bindings." .
|
|
315
311
|
|
|
316
|
-
string:notGreaterThan a ex:Builtin ; ex:kind ex:Test
|
|
312
|
+
string:notGreaterThan a ex:Builtin ; ex:kind ex:Test;
|
|
317
313
|
rdfs:comment "Constraint/test: Unicode codepoint order (<=). No bindings." .
|
|
318
314
|
|
|
319
|
-
string:notLessThan a ex:Builtin ; ex:kind ex:Test
|
|
315
|
+
string:notLessThan a ex:Builtin ; ex:kind ex:Test;
|
|
320
316
|
rdfs:comment "Constraint/test: Unicode codepoint order (>=). No bindings." .
|
|
321
317
|
|
|
322
|
-
string:matches a ex:Builtin ; ex:kind ex:Test
|
|
318
|
+
string:matches a ex:Builtin ; ex:kind ex:Test;
|
|
323
319
|
rdfs:comment "Constraint/test: regex match. Pattern is compiled with swap-style escaping helper. No bindings." .
|
|
324
320
|
|
|
325
|
-
string:notMatches a ex:Builtin ; ex:kind ex:Test
|
|
321
|
+
string:notMatches a ex:Builtin ; ex:kind ex:Test;
|
|
326
322
|
rdfs:comment "Constraint/test: negated regex match. No bindings." .
|
|
327
323
|
|
|
328
324
|
string:replace a ex:Builtin ; ex:kind ex:Function ;
|
package/eyeling.js
CHANGED
|
@@ -727,7 +727,7 @@ const {
|
|
|
727
727
|
|
|
728
728
|
const { lex, N3SyntaxError, decodeN3StringEscapes } = require('./lexer');
|
|
729
729
|
const { Parser } = require('./parser');
|
|
730
|
-
const { liftBlankRuleVars
|
|
730
|
+
const { liftBlankRuleVars } = require('./rules');
|
|
731
731
|
|
|
732
732
|
const { termToN3, tripleToN3 } = require('./printing');
|
|
733
733
|
|
|
@@ -905,8 +905,7 @@ function __makeRuleFromTerms(left, right, isForward) {
|
|
|
905
905
|
}
|
|
906
906
|
|
|
907
907
|
const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
|
|
908
|
-
const [
|
|
909
|
-
const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
|
|
908
|
+
const [premise, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
910
909
|
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
911
910
|
}
|
|
912
911
|
|
|
@@ -2707,8 +2706,12 @@ function evalUnaryMathRel(g, subst, forwardFn, inverseFn /* may be null */) {
|
|
|
2707
2706
|
return [];
|
|
2708
2707
|
}
|
|
2709
2708
|
|
|
2710
|
-
// Fully unbound: treat as satisfiable
|
|
2711
|
-
|
|
2709
|
+
// Fully unbound: do *not* treat as immediately satisfiable.
|
|
2710
|
+
// In goal proving, succeeding with no bindings can let a conjunction
|
|
2711
|
+
// "pass" before other goals bind one side, preventing later evaluation
|
|
2712
|
+
// in the now-solvable direction. Instead, we fail here so the engine's
|
|
2713
|
+
// builtin deferral can retry the goal once variables are bound.
|
|
2714
|
+
if (sIsUnbound && oIsUnbound) return [];
|
|
2712
2715
|
|
|
2713
2716
|
return [];
|
|
2714
2717
|
}
|
|
@@ -5394,6 +5397,26 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
5394
5397
|
return termHasVarOrBlank(tr.s) || termHasVarOrBlank(tr.p) || termHasVarOrBlank(tr.o);
|
|
5395
5398
|
}
|
|
5396
5399
|
|
|
5400
|
+
// Some functional math relations (sin/cos/...) can be used as a pure
|
|
5401
|
+
// satisfiability check. When *both* sides are unbound we avoid infinite
|
|
5402
|
+
// enumeration by producing no bindings, but we still want the conjunction
|
|
5403
|
+
// to succeed once it has been fully deferred to the end.
|
|
5404
|
+
function isSatisfiableWhenFullyUnbound(pIriVal) {
|
|
5405
|
+
return (
|
|
5406
|
+
pIriVal === MATH_NS + 'sin' ||
|
|
5407
|
+
pIriVal === MATH_NS + 'cos' ||
|
|
5408
|
+
pIriVal === MATH_NS + 'tan' ||
|
|
5409
|
+
pIriVal === MATH_NS + 'asin' ||
|
|
5410
|
+
pIriVal === MATH_NS + 'acos' ||
|
|
5411
|
+
pIriVal === MATH_NS + 'atan' ||
|
|
5412
|
+
pIriVal === MATH_NS + 'sinh' ||
|
|
5413
|
+
pIriVal === MATH_NS + 'cosh' ||
|
|
5414
|
+
pIriVal === MATH_NS + 'tanh' ||
|
|
5415
|
+
pIriVal === MATH_NS + 'degrees' ||
|
|
5416
|
+
pIriVal === MATH_NS + 'negation'
|
|
5417
|
+
);
|
|
5418
|
+
}
|
|
5419
|
+
|
|
5397
5420
|
const initialGoals = Array.isArray(goals) ? goals.slice() : [];
|
|
5398
5421
|
const initialSubst = subst ? { ...subst } : {};
|
|
5399
5422
|
const initialVisited = visited ? visited.slice() : [];
|
|
@@ -5443,7 +5466,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
5443
5466
|
const remaining = max - results.length;
|
|
5444
5467
|
if (remaining <= 0) return results;
|
|
5445
5468
|
const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
|
|
5446
|
-
|
|
5469
|
+
let deltas = evalBuiltin(goal0, {}, facts, backRules, state.depth, varGen, builtinMax);
|
|
5447
5470
|
|
|
5448
5471
|
// If the builtin currently yields no solutions but still contains
|
|
5449
5472
|
// unbound variables, try other goals first (defer). This fixes
|
|
@@ -5467,6 +5490,25 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
5467
5490
|
continue;
|
|
5468
5491
|
}
|
|
5469
5492
|
|
|
5493
|
+
// If we've rotated through the whole conjunction without being able to
|
|
5494
|
+
// make progress, and this is a functional math relation with *both* sides
|
|
5495
|
+
// unbound, treat it as satisfiable once (no bindings) rather than failing
|
|
5496
|
+
// the whole conjunction.
|
|
5497
|
+
const __fullyUnboundSO =
|
|
5498
|
+
(goal0.s instanceof Var || goal0.s instanceof Blank) &&
|
|
5499
|
+
(goal0.o instanceof Var || goal0.o instanceof Blank) &&
|
|
5500
|
+
parseNum(goal0.s) === null &&
|
|
5501
|
+
parseNum(goal0.o) === null;
|
|
5502
|
+
if (
|
|
5503
|
+
state.canDeferBuiltins &&
|
|
5504
|
+
!deltas.length &&
|
|
5505
|
+
isSatisfiableWhenFullyUnbound(__pv0) &&
|
|
5506
|
+
__fullyUnboundSO &&
|
|
5507
|
+
(!restGoals.length || dc >= state.goals.length)
|
|
5508
|
+
) {
|
|
5509
|
+
deltas = [{}];
|
|
5510
|
+
}
|
|
5511
|
+
|
|
5470
5512
|
const nextStates = [];
|
|
5471
5513
|
for (const delta of deltas) {
|
|
5472
5514
|
const composed = composeSubst(state.subst, delta);
|
|
@@ -5833,8 +5875,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
5833
5875
|
|
|
5834
5876
|
if (left !== null && right !== null) {
|
|
5835
5877
|
if (isFwRuleTriple) {
|
|
5836
|
-
const [
|
|
5837
|
-
const premise = reorderPremiseForConstraints(premise0);
|
|
5878
|
+
const [premise, conclusion] = liftBlankRuleVars(left, right);
|
|
5838
5879
|
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
5839
5880
|
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
5840
5881
|
|
|
@@ -6864,7 +6905,7 @@ const {
|
|
|
6864
6905
|
} = require('./prelude');
|
|
6865
6906
|
|
|
6866
6907
|
const { N3SyntaxError } = require('./lexer');
|
|
6867
|
-
const { liftBlankRuleVars
|
|
6908
|
+
const { liftBlankRuleVars } = require('./rules');
|
|
6868
6909
|
|
|
6869
6910
|
class Parser {
|
|
6870
6911
|
constructor(tokens) {
|
|
@@ -7484,8 +7525,9 @@ class Parser {
|
|
|
7484
7525
|
|
|
7485
7526
|
const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
7486
7527
|
|
|
7487
|
-
//
|
|
7488
|
-
|
|
7528
|
+
// Keep premise order as written; the engine may defer some builtins in
|
|
7529
|
+
// forward rules when they cannot yet run due to unbound variables.
|
|
7530
|
+
const premise = premise0;
|
|
7489
7531
|
|
|
7490
7532
|
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
7491
7533
|
}
|
|
@@ -8062,11 +8104,6 @@ module.exports = { termToN3, tripleToN3 };
|
|
|
8062
8104
|
'use strict';
|
|
8063
8105
|
|
|
8064
8106
|
const {
|
|
8065
|
-
MATH_NS,
|
|
8066
|
-
LIST_NS,
|
|
8067
|
-
LOG_NS,
|
|
8068
|
-
STRING_NS,
|
|
8069
|
-
Iri,
|
|
8070
8107
|
Var,
|
|
8071
8108
|
Blank,
|
|
8072
8109
|
ListTerm,
|
|
@@ -8122,81 +8159,10 @@ function liftBlankRuleVars(premise, conclusion) {
|
|
|
8122
8159
|
return [newPremise, conclusion];
|
|
8123
8160
|
}
|
|
8124
8161
|
|
|
8125
|
-
function isConstraintBuiltin(tr) {
|
|
8126
|
-
if (!(tr.p instanceof Iri)) return false;
|
|
8127
|
-
const v = tr.p.value;
|
|
8128
|
-
|
|
8129
|
-
// math: numeric comparisons (no new bindings, just tests)
|
|
8130
|
-
if (
|
|
8131
|
-
v === MATH_NS + 'equalTo' ||
|
|
8132
|
-
v === MATH_NS + 'greaterThan' ||
|
|
8133
|
-
v === MATH_NS + 'lessThan' ||
|
|
8134
|
-
v === MATH_NS + 'notEqualTo' ||
|
|
8135
|
-
v === MATH_NS + 'notGreaterThan' ||
|
|
8136
|
-
v === MATH_NS + 'notLessThan'
|
|
8137
|
-
) {
|
|
8138
|
-
return true;
|
|
8139
|
-
}
|
|
8140
|
-
|
|
8141
|
-
// list: membership test with no bindings
|
|
8142
|
-
if (v === LIST_NS + 'notMember') {
|
|
8143
|
-
return true;
|
|
8144
|
-
}
|
|
8145
|
-
|
|
8146
|
-
// log: tests that are purely constraints (no new bindings)
|
|
8147
|
-
if (
|
|
8148
|
-
v === LOG_NS + 'forAllIn' ||
|
|
8149
|
-
v === LOG_NS + 'notEqualTo' ||
|
|
8150
|
-
v === LOG_NS + 'notIncludes' ||
|
|
8151
|
-
v === LOG_NS + 'outputString'
|
|
8152
|
-
) {
|
|
8153
|
-
return true;
|
|
8154
|
-
}
|
|
8155
|
-
|
|
8156
|
-
// string: relational / membership style tests (no bindings)
|
|
8157
|
-
if (
|
|
8158
|
-
v === STRING_NS + 'contains' ||
|
|
8159
|
-
v === STRING_NS + 'containsIgnoringCase' ||
|
|
8160
|
-
v === STRING_NS + 'endsWith' ||
|
|
8161
|
-
v === STRING_NS + 'equalIgnoringCase' ||
|
|
8162
|
-
v === STRING_NS + 'greaterThan' ||
|
|
8163
|
-
v === STRING_NS + 'lessThan' ||
|
|
8164
|
-
v === STRING_NS + 'matches' ||
|
|
8165
|
-
v === STRING_NS + 'notEqualIgnoringCase' ||
|
|
8166
|
-
v === STRING_NS + 'notGreaterThan' ||
|
|
8167
|
-
v === STRING_NS + 'notLessThan' ||
|
|
8168
|
-
v === STRING_NS + 'notMatches' ||
|
|
8169
|
-
v === STRING_NS + 'startsWith'
|
|
8170
|
-
) {
|
|
8171
|
-
return true;
|
|
8172
|
-
}
|
|
8173
|
-
|
|
8174
|
-
return false;
|
|
8175
|
-
}
|
|
8176
|
-
|
|
8177
|
-
// Move constraint builtins to the end of the rule premise.
|
|
8178
|
-
// This is a simple "delaying" strategy similar in spirit to Prolog's when/2:
|
|
8179
|
-
// - normal goals first (can bind variables),
|
|
8180
|
-
// - pure test / constraint builtins last (checked once bindings are in place).
|
|
8181
|
-
function reorderPremiseForConstraints(premise) {
|
|
8182
|
-
if (!premise || premise.length === 0) return premise;
|
|
8183
|
-
|
|
8184
|
-
const normal = [];
|
|
8185
|
-
const delayed = [];
|
|
8186
|
-
|
|
8187
|
-
for (const tr of premise) {
|
|
8188
|
-
if (isConstraintBuiltin(tr)) delayed.push(tr);
|
|
8189
|
-
else normal.push(tr);
|
|
8190
|
-
}
|
|
8191
|
-
return normal.concat(delayed);
|
|
8192
|
-
}
|
|
8193
|
-
|
|
8194
8162
|
// ===========================================================================
|
|
8195
8163
|
|
|
8196
8164
|
module.exports = {
|
|
8197
8165
|
liftBlankRuleVars,
|
|
8198
|
-
isConstraintBuiltin,
|
|
8199
|
-
reorderPremiseForConstraints,
|
|
8200
8166
|
};
|
|
8201
8167
|
|
|
8202
8168
|
};
|
package/lib/engine.js
CHANGED
|
@@ -42,7 +42,7 @@ const {
|
|
|
42
42
|
|
|
43
43
|
const { lex, N3SyntaxError, decodeN3StringEscapes } = require('./lexer');
|
|
44
44
|
const { Parser } = require('./parser');
|
|
45
|
-
const { liftBlankRuleVars
|
|
45
|
+
const { liftBlankRuleVars } = require('./rules');
|
|
46
46
|
|
|
47
47
|
const { termToN3, tripleToN3 } = require('./printing');
|
|
48
48
|
|
|
@@ -220,8 +220,7 @@ function __makeRuleFromTerms(left, right, isForward) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
const headBlankLabels = collectBlankLabelsInTriples(rawConclusion);
|
|
223
|
-
const [
|
|
224
|
-
const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
|
|
223
|
+
const [premise, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
225
224
|
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
226
225
|
}
|
|
227
226
|
|
|
@@ -2022,8 +2021,12 @@ function evalUnaryMathRel(g, subst, forwardFn, inverseFn /* may be null */) {
|
|
|
2022
2021
|
return [];
|
|
2023
2022
|
}
|
|
2024
2023
|
|
|
2025
|
-
// Fully unbound: treat as satisfiable
|
|
2026
|
-
|
|
2024
|
+
// Fully unbound: do *not* treat as immediately satisfiable.
|
|
2025
|
+
// In goal proving, succeeding with no bindings can let a conjunction
|
|
2026
|
+
// "pass" before other goals bind one side, preventing later evaluation
|
|
2027
|
+
// in the now-solvable direction. Instead, we fail here so the engine's
|
|
2028
|
+
// builtin deferral can retry the goal once variables are bound.
|
|
2029
|
+
if (sIsUnbound && oIsUnbound) return [];
|
|
2027
2030
|
|
|
2028
2031
|
return [];
|
|
2029
2032
|
}
|
|
@@ -4709,6 +4712,26 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
4709
4712
|
return termHasVarOrBlank(tr.s) || termHasVarOrBlank(tr.p) || termHasVarOrBlank(tr.o);
|
|
4710
4713
|
}
|
|
4711
4714
|
|
|
4715
|
+
// Some functional math relations (sin/cos/...) can be used as a pure
|
|
4716
|
+
// satisfiability check. When *both* sides are unbound we avoid infinite
|
|
4717
|
+
// enumeration by producing no bindings, but we still want the conjunction
|
|
4718
|
+
// to succeed once it has been fully deferred to the end.
|
|
4719
|
+
function isSatisfiableWhenFullyUnbound(pIriVal) {
|
|
4720
|
+
return (
|
|
4721
|
+
pIriVal === MATH_NS + 'sin' ||
|
|
4722
|
+
pIriVal === MATH_NS + 'cos' ||
|
|
4723
|
+
pIriVal === MATH_NS + 'tan' ||
|
|
4724
|
+
pIriVal === MATH_NS + 'asin' ||
|
|
4725
|
+
pIriVal === MATH_NS + 'acos' ||
|
|
4726
|
+
pIriVal === MATH_NS + 'atan' ||
|
|
4727
|
+
pIriVal === MATH_NS + 'sinh' ||
|
|
4728
|
+
pIriVal === MATH_NS + 'cosh' ||
|
|
4729
|
+
pIriVal === MATH_NS + 'tanh' ||
|
|
4730
|
+
pIriVal === MATH_NS + 'degrees' ||
|
|
4731
|
+
pIriVal === MATH_NS + 'negation'
|
|
4732
|
+
);
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4712
4735
|
const initialGoals = Array.isArray(goals) ? goals.slice() : [];
|
|
4713
4736
|
const initialSubst = subst ? { ...subst } : {};
|
|
4714
4737
|
const initialVisited = visited ? visited.slice() : [];
|
|
@@ -4758,7 +4781,7 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
4758
4781
|
const remaining = max - results.length;
|
|
4759
4782
|
if (remaining <= 0) return results;
|
|
4760
4783
|
const builtinMax = Number.isFinite(remaining) && !restGoals.length ? remaining : undefined;
|
|
4761
|
-
|
|
4784
|
+
let deltas = evalBuiltin(goal0, {}, facts, backRules, state.depth, varGen, builtinMax);
|
|
4762
4785
|
|
|
4763
4786
|
// If the builtin currently yields no solutions but still contains
|
|
4764
4787
|
// unbound variables, try other goals first (defer). This fixes
|
|
@@ -4782,6 +4805,25 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
4782
4805
|
continue;
|
|
4783
4806
|
}
|
|
4784
4807
|
|
|
4808
|
+
// If we've rotated through the whole conjunction without being able to
|
|
4809
|
+
// make progress, and this is a functional math relation with *both* sides
|
|
4810
|
+
// unbound, treat it as satisfiable once (no bindings) rather than failing
|
|
4811
|
+
// the whole conjunction.
|
|
4812
|
+
const __fullyUnboundSO =
|
|
4813
|
+
(goal0.s instanceof Var || goal0.s instanceof Blank) &&
|
|
4814
|
+
(goal0.o instanceof Var || goal0.o instanceof Blank) &&
|
|
4815
|
+
parseNum(goal0.s) === null &&
|
|
4816
|
+
parseNum(goal0.o) === null;
|
|
4817
|
+
if (
|
|
4818
|
+
state.canDeferBuiltins &&
|
|
4819
|
+
!deltas.length &&
|
|
4820
|
+
isSatisfiableWhenFullyUnbound(__pv0) &&
|
|
4821
|
+
__fullyUnboundSO &&
|
|
4822
|
+
(!restGoals.length || dc >= state.goals.length)
|
|
4823
|
+
) {
|
|
4824
|
+
deltas = [{}];
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4785
4827
|
const nextStates = [];
|
|
4786
4828
|
for (const delta of deltas) {
|
|
4787
4829
|
const composed = composeSubst(state.subst, delta);
|
|
@@ -5148,8 +5190,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
5148
5190
|
|
|
5149
5191
|
if (left !== null && right !== null) {
|
|
5150
5192
|
if (isFwRuleTriple) {
|
|
5151
|
-
const [
|
|
5152
|
-
const premise = reorderPremiseForConstraints(premise0);
|
|
5193
|
+
const [premise, conclusion] = liftBlankRuleVars(left, right);
|
|
5153
5194
|
const headBlankLabels = collectBlankLabelsInTriples(conclusion);
|
|
5154
5195
|
const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
|
|
5155
5196
|
|
package/lib/parser.js
CHANGED
|
@@ -40,7 +40,7 @@ const {
|
|
|
40
40
|
} = require('./prelude');
|
|
41
41
|
|
|
42
42
|
const { N3SyntaxError } = require('./lexer');
|
|
43
|
-
const { liftBlankRuleVars
|
|
43
|
+
const { liftBlankRuleVars } = require('./rules');
|
|
44
44
|
|
|
45
45
|
class Parser {
|
|
46
46
|
constructor(tokens) {
|
|
@@ -660,8 +660,9 @@ class Parser {
|
|
|
660
660
|
|
|
661
661
|
const [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
|
|
662
662
|
|
|
663
|
-
//
|
|
664
|
-
|
|
663
|
+
// Keep premise order as written; the engine may defer some builtins in
|
|
664
|
+
// forward rules when they cannot yet run due to unbound variables.
|
|
665
|
+
const premise = premise0;
|
|
665
666
|
|
|
666
667
|
return new Rule(premise, conclusion, isForward, isFuse, headBlankLabels);
|
|
667
668
|
}
|
package/lib/rules.js
CHANGED
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
const {
|
|
11
|
-
MATH_NS,
|
|
12
|
-
LIST_NS,
|
|
13
|
-
LOG_NS,
|
|
14
|
-
STRING_NS,
|
|
15
|
-
Iri,
|
|
16
11
|
Var,
|
|
17
12
|
Blank,
|
|
18
13
|
ListTerm,
|
|
@@ -68,79 +63,8 @@ function liftBlankRuleVars(premise, conclusion) {
|
|
|
68
63
|
return [newPremise, conclusion];
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
function isConstraintBuiltin(tr) {
|
|
72
|
-
if (!(tr.p instanceof Iri)) return false;
|
|
73
|
-
const v = tr.p.value;
|
|
74
|
-
|
|
75
|
-
// math: numeric comparisons (no new bindings, just tests)
|
|
76
|
-
if (
|
|
77
|
-
v === MATH_NS + 'equalTo' ||
|
|
78
|
-
v === MATH_NS + 'greaterThan' ||
|
|
79
|
-
v === MATH_NS + 'lessThan' ||
|
|
80
|
-
v === MATH_NS + 'notEqualTo' ||
|
|
81
|
-
v === MATH_NS + 'notGreaterThan' ||
|
|
82
|
-
v === MATH_NS + 'notLessThan'
|
|
83
|
-
) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// list: membership test with no bindings
|
|
88
|
-
if (v === LIST_NS + 'notMember') {
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// log: tests that are purely constraints (no new bindings)
|
|
93
|
-
if (
|
|
94
|
-
v === LOG_NS + 'forAllIn' ||
|
|
95
|
-
v === LOG_NS + 'notEqualTo' ||
|
|
96
|
-
v === LOG_NS + 'notIncludes' ||
|
|
97
|
-
v === LOG_NS + 'outputString'
|
|
98
|
-
) {
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// string: relational / membership style tests (no bindings)
|
|
103
|
-
if (
|
|
104
|
-
v === STRING_NS + 'contains' ||
|
|
105
|
-
v === STRING_NS + 'containsIgnoringCase' ||
|
|
106
|
-
v === STRING_NS + 'endsWith' ||
|
|
107
|
-
v === STRING_NS + 'equalIgnoringCase' ||
|
|
108
|
-
v === STRING_NS + 'greaterThan' ||
|
|
109
|
-
v === STRING_NS + 'lessThan' ||
|
|
110
|
-
v === STRING_NS + 'matches' ||
|
|
111
|
-
v === STRING_NS + 'notEqualIgnoringCase' ||
|
|
112
|
-
v === STRING_NS + 'notGreaterThan' ||
|
|
113
|
-
v === STRING_NS + 'notLessThan' ||
|
|
114
|
-
v === STRING_NS + 'notMatches' ||
|
|
115
|
-
v === STRING_NS + 'startsWith'
|
|
116
|
-
) {
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Move constraint builtins to the end of the rule premise.
|
|
124
|
-
// This is a simple "delaying" strategy similar in spirit to Prolog's when/2:
|
|
125
|
-
// - normal goals first (can bind variables),
|
|
126
|
-
// - pure test / constraint builtins last (checked once bindings are in place).
|
|
127
|
-
function reorderPremiseForConstraints(premise) {
|
|
128
|
-
if (!premise || premise.length === 0) return premise;
|
|
129
|
-
|
|
130
|
-
const normal = [];
|
|
131
|
-
const delayed = [];
|
|
132
|
-
|
|
133
|
-
for (const tr of premise) {
|
|
134
|
-
if (isConstraintBuiltin(tr)) delayed.push(tr);
|
|
135
|
-
else normal.push(tr);
|
|
136
|
-
}
|
|
137
|
-
return normal.concat(delayed);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
66
|
// ===========================================================================
|
|
141
67
|
|
|
142
68
|
module.exports = {
|
|
143
69
|
liftBlankRuleVars,
|
|
144
|
-
isConstraintBuiltin,
|
|
145
|
-
reorderPremiseForConstraints,
|
|
146
70
|
};
|