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 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
- Because these are pure tests, Eyeling treats them as **constraint builtins** and tends to push them to the end of forward-rule premises so they’re checked after other goals bind variables.
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. This is treated as a constraint builtin.
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
- This is treated as a constraint builtin.
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 treated as a constraint builtin (it shouldn’t drive search; it should merely validate that strings exist once other reasoning has produced them).
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
 
@@ -17,15 +17,13 @@
17
17
  #
18
18
  # Notes:
19
19
  # - ex:kind is intentionally coarse:
20
- # ex:Test = succeeds/fails, no new variable bindings (a "constraint")
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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 ; ex:isConstraint true ;
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, reorderPremiseForConstraints, isConstraintBuiltin } = require('./rules');
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 [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
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 (avoid infinite enumeration)
2711
- if (sIsUnbound && oIsUnbound) return [{ ...subst }];
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
- const deltas = evalBuiltin(goal0, {}, facts, backRules, state.depth, varGen, builtinMax);
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 [premise0, conclusion] = liftBlankRuleVars(left, right);
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, reorderPremiseForConstraints } = require('./rules');
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
- // Reorder constraints for *forward* rules.
7488
- const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
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, reorderPremiseForConstraints, isConstraintBuiltin } = require('./rules');
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 [premise0, conclusion] = liftBlankRuleVars(rawPremise, rawConclusion);
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 (avoid infinite enumeration)
2026
- if (sIsUnbound && oIsUnbound) return [{ ...subst }];
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
- const deltas = evalBuiltin(goal0, {}, facts, backRules, state.depth, varGen, builtinMax);
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 [premise0, conclusion] = liftBlankRuleVars(left, right);
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, reorderPremiseForConstraints } = require('./rules');
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
- // Reorder constraints for *forward* rules.
664
- const premise = isForward ? reorderPremiseForConstraints(premise0) : premise0;
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [