eyeling 1.6.15 → 1.6.17

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.
Files changed (2) hide show
  1. package/eyeling.js +325 -509
  2. package/package.json +1 -1
package/eyeling.js CHANGED
@@ -1993,6 +1993,10 @@ function applySubstTriple(tr, s) {
1993
1993
  return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
1994
1994
  }
1995
1995
 
1996
+ function iriValue(t) {
1997
+ return t instanceof Iri ? t.value : null;
1998
+ }
1999
+
1996
2000
  function unifyOpenWithList(prefix, tailv, ys, subst) {
1997
2001
  if (ys.length < prefix.length) return null;
1998
2002
  let s2 = { ...subst };
@@ -2041,6 +2045,16 @@ function unifyFormulaTriples(xs, ys, subst) {
2041
2045
  }
2042
2046
 
2043
2047
  function unifyTerm(a, b, subst) {
2048
+ return unifyTermWithOptions(a, b, subst, { boolValueEq: true, intDecimalEq: false });
2049
+ }
2050
+
2051
+ function unifyTermListAppend(a, b, subst) {
2052
+ // Keep list:append behavior: allow integer<->decimal exact equality,
2053
+ // but do NOT add boolean-value equivalence (preserves current semantics).
2054
+ return unifyTermWithOptions(a, b, subst, { boolValueEq: false, intDecimalEq: true });
2055
+ }
2056
+
2057
+ function unifyTermWithOptions(a, b, subst, opts) {
2044
2058
  a = applySubstTerm(a, subst);
2045
2059
  b = applySubstTerm(b, subst);
2046
2060
 
@@ -2054,9 +2068,8 @@ function unifyTerm(a, b, subst) {
2054
2068
  s2[v] = t;
2055
2069
  return s2;
2056
2070
  }
2057
-
2058
2071
  if (b instanceof Var) {
2059
- return unifyTerm(b, a, subst);
2072
+ return unifyTermWithOptions(b, a, subst, opts);
2060
2073
  }
2061
2074
 
2062
2075
  // Exact matches
@@ -2064,25 +2077,25 @@ function unifyTerm(a, b, subst) {
2064
2077
  if (a instanceof Literal && b instanceof Literal && a.value === b.value) return { ...subst };
2065
2078
  if (a instanceof Blank && b instanceof Blank && a.label === b.label) return { ...subst };
2066
2079
 
2067
- // String-literal match (RDF 1.1): treat plain strings and xsd:string as equal (but not @lang)
2080
+ // Plain string vs xsd:string equivalence
2068
2081
  if (a instanceof Literal && b instanceof Literal) {
2069
2082
  if (literalsEquivalentAsXsdString(a.value, b.value)) return { ...subst };
2070
2083
  }
2071
2084
 
2072
- // Boolean-value match: treat untyped true/false tokens and xsd:boolean as equal.
2073
- if (a instanceof Literal && b instanceof Literal) {
2085
+ // Boolean-value equivalence (ONLY for normal unifyTerm)
2086
+ if (opts.boolValueEq && a instanceof Literal && b instanceof Literal) {
2074
2087
  const ai = parseBooleanLiteralInfo(a);
2075
2088
  const bi = parseBooleanLiteralInfo(b);
2076
2089
  if (ai && bi && ai.value === bi.value) return { ...subst };
2077
2090
  }
2078
2091
 
2079
- // Numeric-value match for literals, BUT ONLY when datatypes agree (or infer to agree)
2092
+ // Numeric-value match:
2093
+ // - always allow equality when datatype matches (existing behavior)
2094
+ // - optionally allow integer<->decimal exact equality (list:append only)
2080
2095
  if (a instanceof Literal && b instanceof Literal) {
2081
2096
  const ai = parseNumericLiteralInfo(a);
2082
2097
  const bi = parseNumericLiteralInfo(b);
2083
-
2084
2098
  if (ai && bi) {
2085
- // same datatype: keep existing behavior
2086
2099
  if (ai.dt === bi.dt) {
2087
2100
  if (ai.kind === 'bigint' && bi.kind === 'bigint') {
2088
2101
  if (ai.value === bi.value) return { ...subst };
@@ -2092,101 +2105,18 @@ function unifyTerm(a, b, subst) {
2092
2105
  if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
2093
2106
  }
2094
2107
  }
2095
- }
2096
- }
2097
-
2098
- // Open list vs concrete list
2099
- if (a instanceof OpenListTerm && b instanceof ListTerm) {
2100
- return unifyOpenWithList(a.prefix, a.tailVar, b.elems, subst);
2101
- }
2102
- if (a instanceof ListTerm && b instanceof OpenListTerm) {
2103
- return unifyOpenWithList(b.prefix, b.tailVar, a.elems, subst);
2104
- }
2105
2108
 
2106
- // Open list vs open list (same tail var)
2107
- if (a instanceof OpenListTerm && b instanceof OpenListTerm) {
2108
- if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length) return null;
2109
- let s2 = { ...subst };
2110
- for (let i = 0; i < a.prefix.length; i++) {
2111
- s2 = unifyTerm(a.prefix[i], b.prefix[i], s2);
2112
- if (s2 === null) return null;
2113
- }
2114
- return s2;
2115
- }
2116
-
2117
- // List terms
2118
- if (a instanceof ListTerm && b instanceof ListTerm) {
2119
- if (a.elems.length !== b.elems.length) return null;
2120
- let s2 = { ...subst };
2121
- for (let i = 0; i < a.elems.length; i++) {
2122
- s2 = unifyTerm(a.elems[i], b.elems[i], s2);
2123
- if (s2 === null) return null;
2124
- }
2125
- return s2;
2126
- }
2127
-
2128
- // Formulas:
2129
- // 1) If they are alpha-equivalent, succeed without leaking internal bindings.
2130
- // 2) Otherwise fall back to full unification (may bind vars).
2131
- if (a instanceof FormulaTerm && b instanceof FormulaTerm) {
2132
- if (alphaEqFormulaTriples(a.triples, b.triples)) return { ...subst };
2133
- return unifyFormulaTriples(a.triples, b.triples, subst);
2134
- }
2135
- return null;
2136
- }
2137
-
2138
- function unifyTermListAppend(a, b, subst) {
2139
- a = applySubstTerm(a, subst);
2140
- b = applySubstTerm(b, subst);
2141
-
2142
- // Variable binding (same as unifyTerm)
2143
- if (a instanceof Var) {
2144
- const v = a.name;
2145
- const t = b;
2146
- if (t instanceof Var && t.name === v) return { ...subst };
2147
- if (containsVarTerm(t, v)) return null;
2148
- const s2 = { ...subst };
2149
- s2[v] = t;
2150
- return s2;
2151
- }
2152
- if (b instanceof Var) return unifyTermListAppend(b, a, subst);
2153
-
2154
- // Exact matches
2155
- if (a instanceof Iri && b instanceof Iri && a.value === b.value) return { ...subst };
2156
- if (a instanceof Literal && b instanceof Literal && a.value === b.value) return { ...subst };
2157
- if (a instanceof Blank && b instanceof Blank && a.label === b.label) return { ...subst };
2158
-
2159
- // Plain string vs xsd:string equivalence
2160
- if (a instanceof Literal && b instanceof Literal) {
2161
- if (literalsEquivalentAsXsdString(a.value, b.value)) return { ...subst };
2162
- }
2163
-
2164
- // Numeric match: same-dt OR integer<->decimal exact equality (for list:append only)
2165
- if (a instanceof Literal && b instanceof Literal) {
2166
- const ai = parseNumericLiteralInfo(a);
2167
- const bi = parseNumericLiteralInfo(b);
2168
- if (ai && bi) {
2169
- // same datatype
2170
- if (ai.dt === bi.dt) {
2171
- if (ai.kind === 'bigint' && bi.kind === 'bigint') {
2172
- if (ai.value === bi.value) return { ...subst };
2173
- } else {
2174
- const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
2175
- const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
2176
- if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
2177
- }
2178
- }
2179
-
2180
- // integer <-> decimal exact equality
2181
- const intDt = XSD_NS + 'integer';
2182
- const decDt = XSD_NS + 'decimal';
2183
- if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
2184
- const intInfo = ai.dt === intDt ? ai : bi;
2185
- const decInfo = ai.dt === decDt ? ai : bi;
2186
- const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
2187
- if (dec) {
2188
- const scaledInt = intInfo.value * pow10n(dec.scale);
2189
- if (scaledInt === dec.num) return { ...subst };
2109
+ if (opts.intDecimalEq) {
2110
+ const intDt = XSD_NS + 'integer';
2111
+ const decDt = XSD_NS + 'decimal';
2112
+ if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
2113
+ const intInfo = ai.dt === intDt ? ai : bi; // bigint
2114
+ const decInfo = ai.dt === decDt ? ai : bi; // number + lexStr
2115
+ const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
2116
+ if (dec) {
2117
+ const scaledInt = intInfo.value * pow10n(dec.scale);
2118
+ if (scaledInt === dec.num) return { ...subst };
2119
+ }
2190
2120
  }
2191
2121
  }
2192
2122
  }
@@ -2205,7 +2135,7 @@ function unifyTermListAppend(a, b, subst) {
2205
2135
  if (a.tailVar !== b.tailVar || a.prefix.length !== b.prefix.length) return null;
2206
2136
  let s2 = { ...subst };
2207
2137
  for (let i = 0; i < a.prefix.length; i++) {
2208
- s2 = unifyTermListAppend(a.prefix[i], b.prefix[i], s2);
2138
+ s2 = unifyTermWithOptions(a.prefix[i], b.prefix[i], s2, opts);
2209
2139
  if (s2 === null) return null;
2210
2140
  }
2211
2141
  return s2;
@@ -2216,7 +2146,7 @@ function unifyTermListAppend(a, b, subst) {
2216
2146
  if (a.elems.length !== b.elems.length) return null;
2217
2147
  let s2 = { ...subst };
2218
2148
  for (let i = 0; i < a.elems.length; i++) {
2219
- s2 = unifyTermListAppend(a.elems[i], b.elems[i], s2);
2149
+ s2 = unifyTermWithOptions(a.elems[i], b.elems[i], s2, opts);
2220
2150
  if (s2 === null) return null;
2221
2151
  }
2222
2152
  return s2;
@@ -2598,6 +2528,10 @@ function parseXsdFloatSpecialLex(s) {
2598
2528
  return null;
2599
2529
  }
2600
2530
 
2531
+ // ============================================================================
2532
+ // Math builtin helpers
2533
+ // ============================================================================
2534
+
2601
2535
  function formatXsdFloatSpecialLex(n) {
2602
2536
  if (n === Infinity) return 'INF';
2603
2537
  if (n === -Infinity) return '-INF';
@@ -2752,6 +2686,10 @@ function pow10n(k) {
2752
2686
  return 10n ** BigInt(k);
2753
2687
  }
2754
2688
 
2689
+ // ============================================================================
2690
+ // Time & duration builtin helpers
2691
+ // ============================================================================
2692
+
2755
2693
  function parseXsdDateTerm(t) {
2756
2694
  if (!(t instanceof Literal)) return null;
2757
2695
  const [lex, dt] = literalParts(t.value);
@@ -2833,7 +2771,7 @@ function parseNumericForCompareTerm(t) {
2833
2771
  }
2834
2772
 
2835
2773
  function cmpNumericInfo(aInfo, bInfo, op) {
2836
- // op is one of ">", "<", ">=", "<="
2774
+ // op is one of ">", "<", ">=", "<=", "==", "!="
2837
2775
  if (!aInfo || !bInfo) return false;
2838
2776
 
2839
2777
  if (aInfo.kind === 'bigint' && bInfo.kind === 'bigint') {
@@ -2858,6 +2796,19 @@ function cmpNumericInfo(aInfo, bInfo, op) {
2858
2796
  return false;
2859
2797
  }
2860
2798
 
2799
+ function evalNumericComparisonBuiltin(g, subst, op) {
2800
+ const aInfo = parseNumericForCompareTerm(g.s);
2801
+ const bInfo = parseNumericForCompareTerm(g.o);
2802
+ if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, op)) return [{ ...subst }];
2803
+
2804
+ if (g.s instanceof ListTerm && g.s.elems.length === 2) {
2805
+ const a2 = parseNumericForCompareTerm(g.s.elems[0]);
2806
+ const b2 = parseNumericForCompareTerm(g.s.elems[1]);
2807
+ if (a2 && b2 && cmpNumericInfo(a2, b2, op)) return [{ ...subst }];
2808
+ }
2809
+ return [];
2810
+ }
2811
+
2861
2812
  function parseNumOrDuration(t) {
2862
2813
  const n = parseNum(t);
2863
2814
  if (n !== null) return n;
@@ -2886,35 +2837,10 @@ function parseNumOrDuration(t) {
2886
2837
 
2887
2838
  function formatDurationLiteralFromSeconds(secs) {
2888
2839
  const neg = secs < 0;
2889
- const absSecs = Math.abs(secs);
2890
- const days = Math.round(absSecs / 86400.0);
2891
- const lex = neg ? `" -P${days}D"` : `"P${days}D"`;
2892
- const cleanLex = neg ? `" -P${days}D"` : `"P${days}D"`; // minor detail; we just follow shape
2893
- const lex2 = neg ? `" -P${days}D"` : `"P${days}D"`;
2894
- const actualLex = neg ? `" -P${days}D"` : `"P${days}D"`;
2895
- // keep simpler, no spaces:
2896
- const finalLex = neg ? `" -P${days}D"` : `"P${days}D"`;
2840
+ const days = Math.round(Math.abs(secs) / 86400.0);
2897
2841
  const literalLex = neg ? `"-P${days}D"` : `"P${days}D"`;
2898
2842
  return new Literal(`${literalLex}^^<${XSD_NS}duration>`);
2899
2843
  }
2900
-
2901
- function listAppendSplit(parts, resElems, subst) {
2902
- if (!parts.length) {
2903
- if (!resElems.length) return [{ ...subst }];
2904
- return [];
2905
- }
2906
- const out = [];
2907
- const n = resElems.length;
2908
- for (let k = 0; k <= n; k++) {
2909
- const left = new ListTerm(resElems.slice(0, k));
2910
- let s1 = unifyTermListAppend(parts[0], left, subst);
2911
- if (s1 === null) continue;
2912
- const restElems = resElems.slice(k);
2913
- out.push(...listAppendSplit(parts.slice(1), restElems, s1));
2914
- }
2915
- return out;
2916
- }
2917
-
2918
2844
  function numEqualTerm(t, n, eps = 1e-9) {
2919
2845
  const v = parseNum(t);
2920
2846
  if (v === null) return false;
@@ -3092,174 +3018,138 @@ function evalUnaryMathRel(g, subst, forwardFn, inverseFn /* may be null */) {
3092
3018
  }
3093
3019
 
3094
3020
  // ============================================================================
3095
- // Backward proof & builtins mutual recursion — declarations first
3021
+ // List builtin helpers
3096
3022
  // ============================================================================
3097
3023
 
3098
- function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3099
- const g = applySubstTriple(goal, subst);
3100
-
3101
- function hashLiteral(t, algo) {
3102
- // Accept only literals, interpret lexical form as UTF-8 string
3103
- if (!(t instanceof Literal)) return null;
3104
- const [lex, _dt] = literalParts(t.value);
3105
- const input = stripQuotes(lex);
3106
- try {
3107
- const digest = nodeCrypto.createHash(algo).update(input, 'utf8').digest('hex');
3108
- // plain string literal with the hex digest
3109
- return new Literal(JSON.stringify(digest));
3110
- } catch (e) {
3111
- return null;
3112
- }
3024
+ function listAppendSplit(parts, resElems, subst) {
3025
+ if (!parts.length) {
3026
+ if (!resElems.length) return [{ ...subst }];
3027
+ return [];
3113
3028
  }
3114
-
3115
- // -----------------------------------------------------------------
3116
- // 4.1 crypto: builtins
3117
- // -----------------------------------------------------------------
3118
-
3119
- // crypto:sha
3120
- // true iff ?o is the SHA-1 hash of the subject string.
3121
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'sha') {
3122
- const lit = hashLiteral(g.s, 'sha1');
3123
- if (!lit) return [];
3124
- if (g.o instanceof Var) {
3125
- const s2 = { ...subst };
3126
- s2[g.o.name] = lit;
3127
- return [s2];
3128
- }
3129
- const s2 = unifyTerm(g.o, lit, subst);
3130
- return s2 !== null ? [s2] : [];
3029
+ const out = [];
3030
+ const n = resElems.length;
3031
+ for (let k = 0; k <= n; k++) {
3032
+ const left = new ListTerm(resElems.slice(0, k));
3033
+ let s1 = unifyTermListAppend(parts[0], left, subst);
3034
+ if (s1 === null) continue;
3035
+ const restElems = resElems.slice(k);
3036
+ out.push(...listAppendSplit(parts.slice(1), restElems, s1));
3131
3037
  }
3038
+ return out;
3039
+ }
3132
3040
 
3133
- // crypto:md5
3134
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'md5') {
3135
- const lit = hashLiteral(g.s, 'md5');
3136
- if (!lit) return [];
3137
- if (g.o instanceof Var) {
3138
- const s2 = { ...subst };
3139
- s2[g.o.name] = lit;
3140
- return [s2];
3141
- }
3142
- const s2 = unifyTerm(g.o, lit, subst);
3143
- return s2 !== null ? [s2] : [];
3144
- }
3041
+ function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
3042
+ if (!(sTerm instanceof ListTerm)) return [];
3043
+ if (!sTerm.elems.length) return [];
3044
+ const first = sTerm.elems[0];
3045
+ const s2 = unifyTerm(oTerm, first, subst);
3046
+ return s2 !== null ? [s2] : [];
3047
+ }
3145
3048
 
3146
- // crypto:sha256
3147
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'sha256') {
3148
- const lit = hashLiteral(g.s, 'sha256');
3149
- if (!lit) return [];
3150
- if (g.o instanceof Var) {
3151
- const s2 = { ...subst };
3152
- s2[g.o.name] = lit;
3153
- return [s2];
3154
- }
3155
- const s2 = unifyTerm(g.o, lit, subst);
3049
+ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
3050
+ // Closed list: (a b c) -> (b c)
3051
+ if (sTerm instanceof ListTerm) {
3052
+ if (!sTerm.elems.length) return [];
3053
+ const rest = new ListTerm(sTerm.elems.slice(1));
3054
+ const s2 = unifyTerm(oTerm, rest, subst);
3156
3055
  return s2 !== null ? [s2] : [];
3157
3056
  }
3158
3057
 
3159
- // crypto:sha512
3160
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'sha512') {
3161
- const lit = hashLiteral(g.s, 'sha512');
3162
- if (!lit) return [];
3163
- if (g.o instanceof Var) {
3164
- const s2 = { ...subst };
3165
- s2[g.o.name] = lit;
3166
- return [s2];
3058
+ // Open list: (a b ... ?T) -> (b ... ?T)
3059
+ if (sTerm instanceof OpenListTerm) {
3060
+ if (!sTerm.prefix.length) return [];
3061
+ if (sTerm.prefix.length === 1) {
3062
+ const s2 = unifyTerm(oTerm, new Var(sTerm.tailVar), subst);
3063
+ return s2 !== null ? [s2] : [];
3167
3064
  }
3168
- const s2 = unifyTerm(g.o, lit, subst);
3065
+ const rest = new OpenListTerm(sTerm.prefix.slice(1), sTerm.tailVar);
3066
+ const s2 = unifyTerm(oTerm, rest, subst);
3169
3067
  return s2 !== null ? [s2] : [];
3170
3068
  }
3171
3069
 
3172
- // -----------------------------------------------------------------
3173
- // 4.2 math: builtins
3174
- // -----------------------------------------------------------------
3175
-
3176
- // math:greaterThan
3177
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'greaterThan') {
3178
- const aInfo = parseNumericForCompareTerm(g.s);
3179
- const bInfo = parseNumericForCompareTerm(g.o);
3180
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '>')) return [{ ...subst }];
3181
-
3182
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3183
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3184
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3185
- if (a2 && b2 && cmpNumericInfo(a2, b2, '>')) return [{ ...subst }];
3186
- }
3187
- return [];
3188
- }
3070
+ return [];
3071
+ }
3189
3072
 
3190
- // math:lessThan
3191
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'lessThan') {
3192
- const aInfo = parseNumericForCompareTerm(g.s);
3193
- const bInfo = parseNumericForCompareTerm(g.o);
3194
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '<')) return [{ ...subst }];
3073
+ // ============================================================================
3074
+ // Crypto builtin helpers
3075
+ // ============================================================================
3195
3076
 
3196
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3197
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3198
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3199
- if (a2 && b2 && cmpNumericInfo(a2, b2, '<')) return [{ ...subst }];
3200
- }
3201
- return [];
3077
+ function hashLiteralTerm(t, algo) {
3078
+ if (!(t instanceof Literal)) return null;
3079
+ const [lex] = literalParts(t.value);
3080
+ const input = stripQuotes(lex);
3081
+ try {
3082
+ const digest = nodeCrypto.createHash(algo).update(input, 'utf8').digest('hex');
3083
+ return new Literal(JSON.stringify(digest));
3084
+ } catch (e) {
3085
+ return null;
3202
3086
  }
3087
+ }
3203
3088
 
3204
- // math:notLessThan
3205
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'notLessThan') {
3206
- const aInfo = parseNumericForCompareTerm(g.s);
3207
- const bInfo = parseNumericForCompareTerm(g.o);
3208
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '>=')) return [{ ...subst }];
3209
-
3210
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3211
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3212
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3213
- if (a2 && b2 && cmpNumericInfo(a2, b2, '>=')) return [{ ...subst }];
3214
- }
3215
- return [];
3089
+ function evalCryptoHashBuiltin(g, subst, algo) {
3090
+ const lit = hashLiteralTerm(g.s, algo);
3091
+ if (!lit) return [];
3092
+ if (g.o instanceof Var) {
3093
+ const s2 = { ...subst };
3094
+ s2[g.o.name] = lit;
3095
+ return [s2];
3216
3096
  }
3097
+ const s2 = unifyTerm(g.o, lit, subst);
3098
+ return s2 !== null ? [s2] : [];
3099
+ }
3217
3100
 
3218
- // math:notGreaterThan
3219
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'notGreaterThan') {
3220
- const aInfo = parseNumericForCompareTerm(g.s);
3221
- const bInfo = parseNumericForCompareTerm(g.o);
3222
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '<=')) return [{ ...subst }];
3101
+ // ============================================================================
3102
+ // Builtin evaluation
3103
+ // ============================================================================
3104
+ // Backward proof & builtins mutual recursion — declarations first
3223
3105
 
3224
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3225
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3226
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3227
- if (a2 && b2 && cmpNumericInfo(a2, b2, '<=')) return [{ ...subst }];
3228
- }
3229
- return [];
3230
- }
3106
+ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3107
+ const g = applySubstTriple(goal, subst);
3108
+ const pv = iriValue(g.p);
3109
+ if (pv === null) return null;
3231
3110
 
3232
- // math:equalTo
3233
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'equalTo') {
3234
- const aInfo = parseNumericForCompareTerm(g.s);
3235
- const bInfo = parseNumericForCompareTerm(g.o);
3236
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '==')) return [{ ...subst }];
3111
+ // -----------------------------------------------------------------
3112
+ // 4.1 crypto: builtins
3113
+ // -----------------------------------------------------------------
3237
3114
 
3238
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3239
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3240
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3241
- if (a2 && b2 && cmpNumericInfo(a2, b2, '==')) return [{ ...subst }];
3242
- }
3243
- return [];
3244
- }
3115
+ // crypto:sha, crypto:md5, crypto:sha256, crypto:sha512
3116
+ // Digest builtins. crypto:sha uses SHA-1 per the N3/crypto convention.
3117
+ const cryptoAlgo =
3118
+ pv === CRYPTO_NS + 'sha'
3119
+ ? 'sha1'
3120
+ : pv === CRYPTO_NS + 'md5'
3121
+ ? 'md5'
3122
+ : pv === CRYPTO_NS + 'sha256'
3123
+ ? 'sha256'
3124
+ : pv === CRYPTO_NS + 'sha512'
3125
+ ? 'sha512'
3126
+ : null;
3127
+ if (cryptoAlgo) return evalCryptoHashBuiltin(g, subst, cryptoAlgo);
3245
3128
 
3246
- // math:notEqualTo
3247
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'notEqualTo') {
3248
- const aInfo = parseNumericForCompareTerm(g.s);
3249
- const bInfo = parseNumericForCompareTerm(g.o);
3250
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '!=')) return [{ ...subst }];
3129
+ // -----------------------------------------------------------------
3130
+ // 4.2 math: builtins
3131
+ // -----------------------------------------------------------------
3251
3132
 
3252
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3253
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3254
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3255
- if (a2 && b2 && cmpNumericInfo(a2, b2, '!=')) return [{ ...subst }];
3256
- }
3257
- return [];
3258
- }
3133
+ // math:greaterThan / lessThan / notLessThan / notGreaterThan / equalTo / notEqualTo
3134
+ const mathCmpOp =
3135
+ pv === MATH_NS + 'greaterThan'
3136
+ ? '>'
3137
+ : pv === MATH_NS + 'lessThan'
3138
+ ? '<'
3139
+ : pv === MATH_NS + 'notLessThan'
3140
+ ? '>='
3141
+ : pv === MATH_NS + 'notGreaterThan'
3142
+ ? '<='
3143
+ : pv === MATH_NS + 'equalTo'
3144
+ ? '=='
3145
+ : pv === MATH_NS + 'notEqualTo'
3146
+ ? '!='
3147
+ : null;
3148
+ if (mathCmpOp) return evalNumericComparisonBuiltin(g, subst, mathCmpOp);
3259
3149
 
3260
3150
  // math:sum
3261
3151
  // Schema: ( $s.i+ )+ math:sum $o-
3262
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sum') {
3152
+ if (pv === MATH_NS + 'sum') {
3263
3153
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 2) return [];
3264
3154
  const xs = g.s.elems;
3265
3155
 
@@ -3313,7 +3203,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3313
3203
 
3314
3204
  // math:product
3315
3205
  // Schema: ( $s.i+ )+ math:product $o-
3316
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'product') {
3206
+ if (pv === MATH_NS + 'product') {
3317
3207
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 2) return [];
3318
3208
  const xs = g.s.elems;
3319
3209
 
@@ -3365,7 +3255,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3365
3255
 
3366
3256
  // math:difference
3367
3257
  // Schema: ( $s.1+ $s.2+ )+ math:difference $o-
3368
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'difference') {
3258
+ if (pv === MATH_NS + 'difference') {
3369
3259
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3370
3260
  const [a0, b0] = g.s.elems;
3371
3261
 
@@ -3453,7 +3343,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3453
3343
 
3454
3344
  // math:quotient
3455
3345
  // Schema: ( $s.1+ $s.2+ )+ math:quotient $o-
3456
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'quotient') {
3346
+ if (pv === MATH_NS + 'quotient') {
3457
3347
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3458
3348
  const [a0, b0] = g.s.elems;
3459
3349
 
@@ -3482,7 +3372,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3482
3372
  // math:integerQuotient
3483
3373
  // Schema: ( $a $b ) math:integerQuotient $q
3484
3374
  // Cwm: divide first integer by second integer, ignoring remainder. :contentReference[oaicite:1]{index=1}
3485
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'integerQuotient') {
3375
+ if (pv === MATH_NS + 'integerQuotient') {
3486
3376
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3487
3377
  const [a0, b0] = g.s.elems;
3488
3378
 
@@ -3536,7 +3426,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3536
3426
  }
3537
3427
 
3538
3428
  // math:exponentiation
3539
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'exponentiation') {
3429
+ if (pv === MATH_NS + 'exponentiation') {
3540
3430
  if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3541
3431
  const baseTerm = g.s.elems[0];
3542
3432
  const expTerm = g.s.elems[1];
@@ -3583,7 +3473,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3583
3473
  }
3584
3474
 
3585
3475
  // math:absoluteValue
3586
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'absoluteValue') {
3476
+ if (pv === MATH_NS + 'absoluteValue') {
3587
3477
  const a = parseNum(g.s);
3588
3478
  if (a === null) return [];
3589
3479
 
@@ -3604,58 +3494,58 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3604
3494
  }
3605
3495
 
3606
3496
  // math:acos
3607
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'acos') {
3497
+ if (pv === MATH_NS + 'acos') {
3608
3498
  return evalUnaryMathRel(g, subst, Math.acos, Math.cos);
3609
3499
  }
3610
3500
 
3611
3501
  // math:asin
3612
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'asin') {
3502
+ if (pv === MATH_NS + 'asin') {
3613
3503
  return evalUnaryMathRel(g, subst, Math.asin, Math.sin);
3614
3504
  }
3615
3505
 
3616
3506
  // math:atan
3617
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'atan') {
3507
+ if (pv === MATH_NS + 'atan') {
3618
3508
  return evalUnaryMathRel(g, subst, Math.atan, Math.tan);
3619
3509
  }
3620
3510
 
3621
3511
  // math:sin (inverse uses principal asin)
3622
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sin') {
3512
+ if (pv === MATH_NS + 'sin') {
3623
3513
  return evalUnaryMathRel(g, subst, Math.sin, Math.asin);
3624
3514
  }
3625
3515
 
3626
3516
  // math:cos (inverse uses principal acos)
3627
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'cos') {
3517
+ if (pv === MATH_NS + 'cos') {
3628
3518
  return evalUnaryMathRel(g, subst, Math.cos, Math.acos);
3629
3519
  }
3630
3520
 
3631
3521
  // math:tan (inverse uses principal atan)
3632
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tan') {
3522
+ if (pv === MATH_NS + 'tan') {
3633
3523
  return evalUnaryMathRel(g, subst, Math.tan, Math.atan);
3634
3524
  }
3635
3525
 
3636
3526
  // math:sinh / cosh / tanh (guard for JS availability)
3637
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sinh') {
3527
+ if (pv === MATH_NS + 'sinh') {
3638
3528
  if (typeof Math.sinh !== 'function' || typeof Math.asinh !== 'function') return [];
3639
3529
  return evalUnaryMathRel(g, subst, Math.sinh, Math.asinh);
3640
3530
  }
3641
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'cosh') {
3531
+ if (pv === MATH_NS + 'cosh') {
3642
3532
  if (typeof Math.cosh !== 'function' || typeof Math.acosh !== 'function') return [];
3643
3533
  return evalUnaryMathRel(g, subst, Math.cosh, Math.acosh);
3644
3534
  }
3645
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tanh') {
3535
+ if (pv === MATH_NS + 'tanh') {
3646
3536
  if (typeof Math.tanh !== 'function' || typeof Math.atanh !== 'function') return [];
3647
3537
  return evalUnaryMathRel(g, subst, Math.tanh, Math.atanh);
3648
3538
  }
3649
3539
 
3650
3540
  // math:degrees (inverse is radians)
3651
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'degrees') {
3541
+ if (pv === MATH_NS + 'degrees') {
3652
3542
  const toDeg = (rad) => (rad * 180.0) / Math.PI;
3653
3543
  const toRad = (deg) => (deg * Math.PI) / 180.0;
3654
3544
  return evalUnaryMathRel(g, subst, toDeg, toRad);
3655
3545
  }
3656
3546
 
3657
3547
  // math:negation (inverse is itself)
3658
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'negation') {
3548
+ if (pv === MATH_NS + 'negation') {
3659
3549
  const neg = (x) => -x;
3660
3550
  return evalUnaryMathRel(g, subst, neg, neg);
3661
3551
  }
@@ -3663,7 +3553,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3663
3553
  // math:remainder
3664
3554
  // Subject is a list (dividend divisor); object is the remainder.
3665
3555
  // Schema: ( $a $b ) math:remainder $r
3666
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'remainder') {
3556
+ if (pv === MATH_NS + 'remainder') {
3667
3557
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3668
3558
  const [a0, b0] = g.s.elems;
3669
3559
 
@@ -3713,7 +3603,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3713
3603
  // If there are two such numbers, then the one closest to positive infinity is returned.
3714
3604
  // Schema: $s+ math:rounded $o-
3715
3605
  // Note: spec says $o is xsd:integer, but we also accept any numeric $o that equals the rounded value.
3716
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'rounded') {
3606
+ if (pv === MATH_NS + 'rounded') {
3717
3607
  const a = parseNum(g.s);
3718
3608
  if (a === null) return [];
3719
3609
  if (Number.isNaN(a)) return [];
@@ -3742,7 +3632,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3742
3632
 
3743
3633
  // time:localTime
3744
3634
  // "" time:localTime ?D. binds ?D to “now” as xsd:dateTime.
3745
- if (g.p instanceof Iri && g.p.value === TIME_NS + 'localTime') {
3635
+ if (pv === TIME_NS + 'localTime') {
3746
3636
  const now = getNowLex();
3747
3637
 
3748
3638
  if (g.o instanceof Var) {
@@ -3764,7 +3654,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3764
3654
  // list:append
3765
3655
  // true if and only if $o is the concatenation of all lists $s.i.
3766
3656
  // Schema: ( $s.i?[*] )+ list:append $o?
3767
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'append') {
3657
+ if (pv === LIST_NS + 'append') {
3768
3658
  if (!(g.s instanceof ListTerm)) return [];
3769
3659
  const parts = g.s.elems;
3770
3660
  if (g.o instanceof ListTerm) {
@@ -3785,86 +3675,25 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3785
3675
  return [];
3786
3676
  }
3787
3677
 
3788
- // list:first
3678
+ // list:first and rdf:first
3789
3679
  // true iff $s is a list and $o is the first member of that list.
3790
3680
  // Schema: $s+ list:first $o-
3791
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'first') {
3792
- if (!(g.s instanceof ListTerm)) return [];
3793
- if (!g.s.elems.length) return [];
3794
- const first = g.s.elems[0];
3795
- const s2 = unifyTerm(g.o, first, subst);
3796
- return s2 !== null ? [s2] : [];
3681
+ if (pv === LIST_NS + 'first' || pv === RDF_NS + 'first') {
3682
+ return evalListFirstLikeBuiltin(g.s, g.o, subst);
3797
3683
  }
3798
3684
 
3799
- // list:rest
3685
+ // list:rest and rdf:rest
3800
3686
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
3801
3687
  // Schema: $s+ list:rest $o-
3802
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'rest') {
3803
- // Closed list: (a b c) -> (b c)
3804
- if (g.s instanceof ListTerm) {
3805
- if (!g.s.elems.length) return [];
3806
- const rest = new ListTerm(g.s.elems.slice(1));
3807
- const s2 = unifyTerm(g.o, rest, subst);
3808
- return s2 !== null ? [s2] : [];
3809
- }
3810
-
3811
- // Open list: (a b ... ?T) -> (b ... ?T)
3812
- if (g.s instanceof OpenListTerm) {
3813
- if (!g.s.prefix.length) return []; // can't compute rest without a known head
3814
-
3815
- if (g.s.prefix.length === 1) {
3816
- // (a ... ?T) rest is exactly ?T
3817
- const s2 = unifyTerm(g.o, new Var(g.s.tailVar), subst);
3818
- return s2 !== null ? [s2] : [];
3819
- }
3820
-
3821
- const rest = new OpenListTerm(g.s.prefix.slice(1), g.s.tailVar);
3822
- const s2 = unifyTerm(g.o, rest, subst);
3823
- return s2 !== null ? [s2] : [];
3824
- }
3825
-
3826
- return [];
3827
- }
3828
-
3829
- // rdf:first (alias of list:first)
3830
- // Schema: $s+ rdf:first $o-
3831
- if (g.p instanceof Iri && g.p.value === RDF_NS + 'first') {
3832
- if (!(g.s instanceof ListTerm)) return [];
3833
- if (!g.s.elems.length) return [];
3834
- const first = g.s.elems[0];
3835
- const s2 = unifyTerm(g.o, first, subst);
3836
- return s2 !== null ? [s2] : [];
3837
- }
3838
-
3839
- // rdf:rest (alias of list:rest)
3840
- // Schema: $s+ rdf:rest $o-
3841
- if (g.p instanceof Iri && g.p.value === RDF_NS + 'rest') {
3842
- // Closed list: (a b c) -> (b c)
3843
- if (g.s instanceof ListTerm) {
3844
- if (!g.s.elems.length) return [];
3845
- const rest = new ListTerm(g.s.elems.slice(1));
3846
- const s2 = unifyTerm(g.o, rest, subst);
3847
- return s2 !== null ? [s2] : [];
3848
- }
3849
- // Open list: (a b ... ?T) -> (b ... ?T)
3850
- if (g.s instanceof OpenListTerm) {
3851
- if (!g.s.prefix.length) return [];
3852
- if (g.s.prefix.length === 1) {
3853
- const s2 = unifyTerm(g.o, new Var(g.s.tailVar), subst);
3854
- return s2 !== null ? [s2] : [];
3855
- }
3856
- const rest = new OpenListTerm(g.s.prefix.slice(1), g.s.tailVar);
3857
- const s2 = unifyTerm(g.o, rest, subst);
3858
- return s2 !== null ? [s2] : [];
3859
- }
3860
- return [];
3688
+ if (pv === LIST_NS + 'rest' || pv === RDF_NS + 'rest') {
3689
+ return evalListRestLikeBuiltin(g.s, g.o, subst);
3861
3690
  }
3862
3691
 
3863
3692
  // list:iterate
3864
3693
  // Multi-solution builtin:
3865
3694
  // For a list subject $s, generate solutions by unifying $o with (index value).
3866
3695
  // This allows $o to be a variable (e.g., ?Y) or a pattern (e.g., (?i "Dewey")).
3867
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'iterate') {
3696
+ if (pv === LIST_NS + 'iterate') {
3868
3697
  if (!(g.s instanceof ListTerm)) return [];
3869
3698
  const xs = g.s.elems;
3870
3699
  const outs = [];
@@ -3905,7 +3734,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3905
3734
  // list:last
3906
3735
  // true iff $s is a list and $o is the last member of that list.
3907
3736
  // Schema: $s+ list:last $o-
3908
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'last') {
3737
+ if (pv === LIST_NS + 'last') {
3909
3738
  if (!(g.s instanceof ListTerm)) return [];
3910
3739
  const xs = g.s.elems;
3911
3740
  if (!xs.length) return [];
@@ -3917,7 +3746,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3917
3746
  // list:memberAt
3918
3747
  // true iff $s.1 is a list, $s.2 is a valid index, and $o is the member at that index.
3919
3748
  // Schema: ( $s.1+ $s.2?[*] )+ list:memberAt $o?[*]
3920
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'memberAt') {
3749
+ if (pv === LIST_NS + 'memberAt') {
3921
3750
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3922
3751
  const [listTerm, indexTerm] = g.s.elems;
3923
3752
  if (!(listTerm instanceof ListTerm)) return [];
@@ -3956,7 +3785,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3956
3785
  // list:remove
3957
3786
  // true iff $s.1 is a list and $o is that list with all occurrences of $s.2 removed.
3958
3787
  // Schema: ( $s.1+ $s.2+ )+ list:remove $o-
3959
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'remove') {
3788
+ if (pv === LIST_NS + 'remove') {
3960
3789
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3961
3790
  const [listTerm, itemTerm] = g.s.elems;
3962
3791
  if (!(listTerm instanceof ListTerm)) return [];
@@ -3978,7 +3807,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3978
3807
  }
3979
3808
 
3980
3809
  // list:member
3981
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'member') {
3810
+ if (pv === LIST_NS + 'member') {
3982
3811
  if (!(g.s instanceof ListTerm)) return [];
3983
3812
  const outs = [];
3984
3813
  for (const x of g.s.elems) {
@@ -3989,7 +3818,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3989
3818
  }
3990
3819
 
3991
3820
  // list:in
3992
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'in') {
3821
+ if (pv === LIST_NS + 'in') {
3993
3822
  if (!(g.o instanceof ListTerm)) return [];
3994
3823
  const outs = [];
3995
3824
  for (const x of g.o.elems) {
@@ -4000,7 +3829,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4000
3829
  }
4001
3830
 
4002
3831
  // list:length (strict: do not accept integer<->decimal matches for a ground object)
4003
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'length') {
3832
+ if (pv === LIST_NS + 'length') {
4004
3833
  if (!(g.s instanceof ListTerm)) return [];
4005
3834
  const nTerm = new Literal(String(g.s.elems.length));
4006
3835
 
@@ -4014,7 +3843,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4014
3843
  }
4015
3844
 
4016
3845
  // list:notMember
4017
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'notMember') {
3846
+ if (pv === LIST_NS + 'notMember') {
4018
3847
  if (!(g.s instanceof ListTerm)) return [];
4019
3848
  for (const el of g.s.elems) {
4020
3849
  if (unifyTerm(g.o, el, subst) !== null) return [];
@@ -4023,7 +3852,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4023
3852
  }
4024
3853
 
4025
3854
  // list:reverse
4026
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'reverse') {
3855
+ if (pv === LIST_NS + 'reverse') {
4027
3856
  if (g.s instanceof ListTerm) {
4028
3857
  const rev = [...g.s.elems].reverse();
4029
3858
  const rterm = new ListTerm(rev);
@@ -4040,7 +3869,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4040
3869
  }
4041
3870
 
4042
3871
  // list:sort
4043
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'sort') {
3872
+ if (pv === LIST_NS + 'sort') {
4044
3873
  function cmpTermForSort(a, b) {
4045
3874
  if (a instanceof Literal && b instanceof Literal) {
4046
3875
  const [lexA] = literalParts(a.value);
@@ -4108,7 +3937,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4108
3937
  }
4109
3938
 
4110
3939
  // list:map
4111
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'map') {
3940
+ if (pv === LIST_NS + 'map') {
4112
3941
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4113
3942
  const [inputTerm, predTerm] = g.s.elems;
4114
3943
  if (!(inputTerm instanceof ListTerm)) return [];
@@ -4134,7 +3963,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4134
3963
  }
4135
3964
 
4136
3965
  // list:firstRest
4137
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'firstRest') {
3966
+ if (pv === LIST_NS + 'firstRest') {
4138
3967
  if (g.s instanceof ListTerm) {
4139
3968
  if (!g.s.elems.length) return [];
4140
3969
  const first = g.s.elems[0];
@@ -4172,13 +4001,13 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4172
4001
  // -----------------------------------------------------------------
4173
4002
 
4174
4003
  // log:equalTo
4175
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'equalTo') {
4004
+ if (pv === LOG_NS + 'equalTo') {
4176
4005
  const s2 = unifyTerm(goal.s, goal.o, subst);
4177
4006
  return s2 !== null ? [s2] : [];
4178
4007
  }
4179
4008
 
4180
4009
  // log:notEqualTo
4181
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'notEqualTo') {
4010
+ if (pv === LOG_NS + 'notEqualTo') {
4182
4011
  const s2 = unifyTerm(goal.s, goal.o, subst);
4183
4012
  if (s2 !== null) return [];
4184
4013
  return [{ ...subst }];
@@ -4187,7 +4016,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4187
4016
  // log:dtlit
4188
4017
  // Schema: ( $s.1? $s.2? )? log:dtlit $o?
4189
4018
  // true iff $o is a datatyped literal with string value $s.1 and datatype IRI $s.2
4190
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'dtlit') {
4019
+ if (pv === LOG_NS + 'dtlit') {
4191
4020
  // Fully unbound (both arguments '?'-mode): treat as satisfiable, succeed once.
4192
4021
  // Required by notation3tests "success-fullUnbound-*".
4193
4022
  if (g.s instanceof Var && g.o instanceof Var) return [{ ...subst }];
@@ -4240,7 +4069,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4240
4069
  // log:langlit
4241
4070
  // Schema: ( $s.1? $s.2? )? log:langlit $o?
4242
4071
  // true iff $o is a language-tagged literal with string value $s.1 and language tag $s.2
4243
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'langlit') {
4072
+ if (pv === LOG_NS + 'langlit') {
4244
4073
  // Fully unbound (both arguments '?'-mode): treat as satisfiable, succeed once.
4245
4074
  if (g.s instanceof Var && g.o instanceof Var) return [{ ...subst }];
4246
4075
  const results = [];
@@ -4294,7 +4123,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4294
4123
  }
4295
4124
 
4296
4125
  // log:implies — expose internal forward rules as data
4297
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'implies') {
4126
+ if (pv === LOG_NS + 'implies') {
4298
4127
  const allFw = backRules.__allForwardRules || [];
4299
4128
  const results = [];
4300
4129
 
@@ -4322,7 +4151,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4322
4151
  }
4323
4152
 
4324
4153
  // log:impliedBy — expose internal backward rules as data
4325
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'impliedBy') {
4154
+ if (pv === LOG_NS + 'impliedBy') {
4326
4155
  const allBw = backRules.__allBackwardRules || backRules;
4327
4156
  const results = [];
4328
4157
 
@@ -4352,7 +4181,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4352
4181
 
4353
4182
  // log:notIncludes (EYE-style: "not provable in scope")
4354
4183
  // Delay until we have a frozen scope snapshot to avoid early success.
4355
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'notIncludes') {
4184
+ if (pv === LOG_NS + 'notIncludes') {
4356
4185
  if (!(g.o instanceof FormulaTerm)) return [];
4357
4186
 
4358
4187
  let scopeFacts = null;
@@ -4375,7 +4204,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4375
4204
  }
4376
4205
 
4377
4206
  // log:collectAllIn (scoped)
4378
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'collectAllIn') {
4207
+ if (pv === LOG_NS + 'collectAllIn') {
4379
4208
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
4380
4209
  const [valueTempl, clauseTerm, listTerm] = g.s.elems;
4381
4210
  if (!(clauseTerm instanceof FormulaTerm)) return [];
@@ -4404,7 +4233,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4404
4233
  }
4405
4234
 
4406
4235
  // log:forAllIn (scoped)
4407
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'forAllIn') {
4236
+ if (pv === LOG_NS + 'forAllIn') {
4408
4237
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4409
4238
  const [whereClause, thenClause] = g.s.elems;
4410
4239
  if (!(whereClause instanceof FormulaTerm) || !(thenClause instanceof FormulaTerm)) return [];
@@ -4434,7 +4263,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4434
4263
  }
4435
4264
 
4436
4265
  // log:skolem
4437
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'skolem') {
4266
+ if (pv === LOG_NS + 'skolem') {
4438
4267
  // Subject must be ground; commonly a list, but we allow any ground term.
4439
4268
  if (!isGroundTerm(g.s)) return [];
4440
4269
 
@@ -4451,7 +4280,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4451
4280
  }
4452
4281
 
4453
4282
  // log:uri
4454
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'uri') {
4283
+ if (pv === LOG_NS + 'uri') {
4455
4284
  // Direction 1: subject is an IRI -> object is its string representation
4456
4285
  if (g.s instanceof Iri) {
4457
4286
  const uriStr = g.s.value; // raw IRI string, e.g. "https://www.w3.org"
@@ -4480,7 +4309,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4480
4309
  // -----------------------------------------------------------------
4481
4310
 
4482
4311
  // string:concatenation
4483
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'concatenation') {
4312
+ if (pv === STRING_NS + 'concatenation') {
4484
4313
  if (!(g.s instanceof ListTerm)) return [];
4485
4314
  const parts = [];
4486
4315
  for (const t of g.s.elems) {
@@ -4500,7 +4329,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4500
4329
  }
4501
4330
 
4502
4331
  // string:contains
4503
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'contains') {
4332
+ if (pv === STRING_NS + 'contains') {
4504
4333
  const sStr = termToJsString(g.s);
4505
4334
  const oStr = termToJsString(g.o);
4506
4335
  if (sStr === null || oStr === null) return [];
@@ -4508,7 +4337,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4508
4337
  }
4509
4338
 
4510
4339
  // string:containsIgnoringCase
4511
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'containsIgnoringCase') {
4340
+ if (pv === STRING_NS + 'containsIgnoringCase') {
4512
4341
  const sStr = termToJsString(g.s);
4513
4342
  const oStr = termToJsString(g.o);
4514
4343
  if (sStr === null || oStr === null) return [];
@@ -4516,7 +4345,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4516
4345
  }
4517
4346
 
4518
4347
  // string:endsWith
4519
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'endsWith') {
4348
+ if (pv === STRING_NS + 'endsWith') {
4520
4349
  const sStr = termToJsString(g.s);
4521
4350
  const oStr = termToJsString(g.o);
4522
4351
  if (sStr === null || oStr === null) return [];
@@ -4524,7 +4353,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4524
4353
  }
4525
4354
 
4526
4355
  // string:equalIgnoringCase
4527
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'equalIgnoringCase') {
4356
+ if (pv === STRING_NS + 'equalIgnoringCase') {
4528
4357
  const sStr = termToJsString(g.s);
4529
4358
  const oStr = termToJsString(g.o);
4530
4359
  if (sStr === null || oStr === null) return [];
@@ -4533,7 +4362,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4533
4362
 
4534
4363
  // string:format
4535
4364
  // (limited: only %s and %% are supported, anything else ⇒ builtin fails)
4536
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'format') {
4365
+ if (pv === STRING_NS + 'format') {
4537
4366
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 1) return [];
4538
4367
  const fmtStr = termToJsString(g.s.elems[0]);
4539
4368
  if (fmtStr === null) return [];
@@ -4560,10 +4389,10 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4560
4389
 
4561
4390
  // string:jsonPointer
4562
4391
  // Schema: ( $jsonText $pointer ) string:jsonPointer $value
4563
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'jsonPointer') {
4392
+ if (pv === STRING_NS + 'jsonPointer') {
4564
4393
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4565
4394
 
4566
- const jsonText = termToJsonText(g.s.elems[0]); // <-- changed
4395
+ const jsonText = termToJsonText(g.s.elems[0]);
4567
4396
  const ptr = termToJsStringDecoded(g.s.elems[1]);
4568
4397
  if (jsonText === null || ptr === null) return [];
4569
4398
 
@@ -4575,7 +4404,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4575
4404
  }
4576
4405
 
4577
4406
  // string:greaterThan
4578
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'greaterThan') {
4407
+ if (pv === STRING_NS + 'greaterThan') {
4579
4408
  const sStr = termToJsString(g.s);
4580
4409
  const oStr = termToJsString(g.o);
4581
4410
  if (sStr === null || oStr === null) return [];
@@ -4583,7 +4412,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4583
4412
  }
4584
4413
 
4585
4414
  // string:lessThan
4586
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'lessThan') {
4415
+ if (pv === STRING_NS + 'lessThan') {
4587
4416
  const sStr = termToJsString(g.s);
4588
4417
  const oStr = termToJsString(g.o);
4589
4418
  if (sStr === null || oStr === null) return [];
@@ -4591,7 +4420,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4591
4420
  }
4592
4421
 
4593
4422
  // string:matches
4594
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'matches') {
4423
+ if (pv === STRING_NS + 'matches') {
4595
4424
  const sStr = termToJsString(g.s);
4596
4425
  const pattern = termToJsString(g.o);
4597
4426
  if (sStr === null || pattern === null) return [];
@@ -4606,7 +4435,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4606
4435
  }
4607
4436
 
4608
4437
  // string:notEqualIgnoringCase
4609
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notEqualIgnoringCase') {
4438
+ if (pv === STRING_NS + 'notEqualIgnoringCase') {
4610
4439
  const sStr = termToJsString(g.s);
4611
4440
  const oStr = termToJsString(g.o);
4612
4441
  if (sStr === null || oStr === null) return [];
@@ -4614,7 +4443,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4614
4443
  }
4615
4444
 
4616
4445
  // string:notGreaterThan (≤ in Unicode code order)
4617
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notGreaterThan') {
4446
+ if (pv === STRING_NS + 'notGreaterThan') {
4618
4447
  const sStr = termToJsString(g.s);
4619
4448
  const oStr = termToJsString(g.o);
4620
4449
  if (sStr === null || oStr === null) return [];
@@ -4622,7 +4451,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4622
4451
  }
4623
4452
 
4624
4453
  // string:notLessThan (≥ in Unicode code order)
4625
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notLessThan') {
4454
+ if (pv === STRING_NS + 'notLessThan') {
4626
4455
  const sStr = termToJsString(g.s);
4627
4456
  const oStr = termToJsString(g.o);
4628
4457
  if (sStr === null || oStr === null) return [];
@@ -4630,7 +4459,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4630
4459
  }
4631
4460
 
4632
4461
  // string:notMatches
4633
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notMatches') {
4462
+ if (pv === STRING_NS + 'notMatches') {
4634
4463
  const sStr = termToJsString(g.s);
4635
4464
  const pattern = termToJsString(g.o);
4636
4465
  if (sStr === null || pattern === null) return [];
@@ -4644,7 +4473,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4644
4473
  }
4645
4474
 
4646
4475
  // string:replace
4647
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'replace') {
4476
+ if (pv === STRING_NS + 'replace') {
4648
4477
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
4649
4478
  const dataStr = termToJsString(g.s.elems[0]);
4650
4479
  const searchStr = termToJsString(g.s.elems[1]);
@@ -4672,7 +4501,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4672
4501
  }
4673
4502
 
4674
4503
  // string:scrape
4675
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'scrape') {
4504
+ if (pv === STRING_NS + 'scrape') {
4676
4505
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4677
4506
  const dataStr = termToJsString(g.s.elems[0]);
4678
4507
  const pattern = termToJsString(g.s.elems[1]);
@@ -4701,7 +4530,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4701
4530
  }
4702
4531
 
4703
4532
  // string:startsWith
4704
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'startsWith') {
4533
+ if (pv === STRING_NS + 'startsWith') {
4705
4534
  const sStr = termToJsString(g.s);
4706
4535
  const oStr = termToJsString(g.o);
4707
4536
  if (sStr === null || oStr === null) return [];
@@ -5065,134 +4894,121 @@ function forwardChain(facts, forwardRules, backRules) {
5065
4894
 
5066
4895
  function runFixpoint() {
5067
4896
  let anyChange = false;
4897
+
5068
4898
  while (true) {
5069
4899
  let changed = false;
5070
4900
 
5071
- while (true) {
5072
- let changed = false;
4901
+ for (let i = 0; i < forwardRules.length; i++) {
4902
+ const r = forwardRules[i];
4903
+ const empty = {};
4904
+ const visited = [];
4905
+ const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen);
5073
4906
 
5074
- for (let i = 0; i < forwardRules.length; i++) {
5075
- const r = forwardRules[i];
5076
- const empty = {};
5077
- const visited = [];
5078
-
5079
- const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen);
4907
+ // Inference fuse
4908
+ if (r.isFuse && sols.length) {
4909
+ console.log('# Inference fuse triggered: a { ... } => false. rule fired.');
4910
+ process.exit(2);
4911
+ }
5080
4912
 
5081
- // Inference fuse
5082
- if (r.isFuse && sols.length) {
5083
- console.log('# Inference fuse triggered: a { ... } => false. rule fired.');
5084
- process.exit(2);
5085
- }
4913
+ for (const s of sols) {
4914
+ // IMPORTANT: one skolem map per *rule firing*
4915
+ const skMap = {};
4916
+ const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
4917
+ const fireKey = firingKey(i, instantiatedPremises);
4918
+
4919
+ for (const cpat of r.conclusion) {
4920
+ const instantiated = applySubstTriple(cpat, s);
4921
+
4922
+ const isFwRuleTriple =
4923
+ isLogImplies(instantiated.p) &&
4924
+ ((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
4925
+ (instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm) ||
4926
+ (instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true'));
4927
+
4928
+ const isBwRuleTriple =
4929
+ isLogImpliedBy(instantiated.p) &&
4930
+ ((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
4931
+ (instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true') ||
4932
+ (instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm));
4933
+
4934
+ if (isFwRuleTriple || isBwRuleTriple) {
4935
+ if (!hasFactIndexed(facts, instantiated)) {
4936
+ factList.push(instantiated);
4937
+ pushFactIndexed(facts, instantiated);
4938
+ derivedForward.push(new DerivedFact(instantiated, r, instantiatedPremises.slice(), { ...s }));
4939
+ changed = true;
4940
+ }
5086
4941
 
5087
- for (const s of sols) {
5088
- // IMPORTANT: one skolem map per *rule firing* so head blank nodes
5089
- // (e.g., from [ :p ... ; :q ... ]) stay connected across all head triples.
5090
- const skMap = {};
5091
- const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
5092
- const fireKey = firingKey(i, instantiatedPremises);
5093
-
5094
- for (const cpat of r.conclusion) {
5095
- const instantiated = applySubstTriple(cpat, s);
5096
-
5097
- const isFwRuleTriple =
5098
- isLogImplies(instantiated.p) &&
5099
- ((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
5100
- (instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm) ||
5101
- (instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true'));
5102
-
5103
- const isBwRuleTriple =
5104
- isLogImpliedBy(instantiated.p) &&
5105
- ((instantiated.s instanceof FormulaTerm && instantiated.o instanceof FormulaTerm) ||
5106
- (instantiated.s instanceof FormulaTerm && instantiated.o instanceof Literal && instantiated.o.value === 'true') ||
5107
- (instantiated.s instanceof Literal && instantiated.s.value === 'true' && instantiated.o instanceof FormulaTerm));
5108
-
5109
- if (isFwRuleTriple || isBwRuleTriple) {
5110
- if (!hasFactIndexed(facts, instantiated)) {
5111
- factList.push(instantiated);
5112
- pushFactIndexed(facts, instantiated);
5113
- derivedForward.push(
5114
- new DerivedFact(instantiated, r, instantiatedPremises.slice(), {
5115
- ...s,
5116
- }),
4942
+ // Promote rule-producing triples to live rules, treating literal true as {}.
4943
+ const left =
4944
+ instantiated.s instanceof FormulaTerm
4945
+ ? instantiated.s.triples
4946
+ : instantiated.s instanceof Literal && instantiated.s.value === 'true'
4947
+ ? []
4948
+ : null;
4949
+
4950
+ const right =
4951
+ instantiated.o instanceof FormulaTerm
4952
+ ? instantiated.o.triples
4953
+ : instantiated.o instanceof Literal && instantiated.o.value === 'true'
4954
+ ? []
4955
+ : null;
4956
+
4957
+ if (left !== null && right !== null) {
4958
+ if (isFwRuleTriple) {
4959
+ const [premise0, conclusion] = liftBlankRuleVars(left, right);
4960
+ const premise = reorderPremiseForConstraints(premise0);
4961
+ const headBlankLabels = collectBlankLabelsInTriples(conclusion);
4962
+ const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
4963
+
4964
+ const already = forwardRules.some(
4965
+ (rr) =>
4966
+ rr.isForward === newRule.isForward &&
4967
+ rr.isFuse === newRule.isFuse &&
4968
+ triplesListEqual(rr.premise, newRule.premise) &&
4969
+ triplesListEqual(rr.conclusion, newRule.conclusion),
5117
4970
  );
5118
- changed = true;
5119
- }
5120
-
5121
- // Promote rule-producing triples to live rules, treating literal true as {}.
5122
- const left =
5123
- instantiated.s instanceof FormulaTerm
5124
- ? instantiated.s.triples
5125
- : instantiated.s instanceof Literal && instantiated.s.value === 'true'
5126
- ? []
5127
- : null;
5128
-
5129
- const right =
5130
- instantiated.o instanceof FormulaTerm
5131
- ? instantiated.o.triples
5132
- : instantiated.o instanceof Literal && instantiated.o.value === 'true'
5133
- ? []
5134
- : null;
5135
-
5136
- if (left !== null && right !== null) {
5137
- if (isFwRuleTriple) {
5138
- const [premise0, conclusion] = liftBlankRuleVars(left, right);
5139
- const premise = reorderPremiseForConstraints(premise0);
5140
-
5141
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
5142
- const newRule = new Rule(premise, conclusion, true, false, headBlankLabels);
5143
-
5144
- const already = forwardRules.some(
5145
- (rr) =>
5146
- rr.isForward === newRule.isForward &&
5147
- rr.isFuse === newRule.isFuse &&
5148
- triplesListEqual(rr.premise, newRule.premise) &&
5149
- triplesListEqual(rr.conclusion, newRule.conclusion),
5150
- );
5151
- if (!already) forwardRules.push(newRule);
5152
- } else if (isBwRuleTriple) {
5153
- const [premise, conclusion] = liftBlankRuleVars(right, left);
5154
-
5155
- const headBlankLabels = collectBlankLabelsInTriples(conclusion);
5156
- const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
5157
-
5158
- const already = backRules.some(
5159
- (rr) =>
5160
- rr.isForward === newRule.isForward &&
5161
- rr.isFuse === newRule.isFuse &&
5162
- triplesListEqual(rr.premise, newRule.premise) &&
5163
- triplesListEqual(rr.conclusion, newRule.conclusion),
5164
- );
5165
- if (!already) {
5166
- backRules.push(newRule);
5167
- indexBackRule(backRules, newRule);
5168
- }
4971
+ if (!already) forwardRules.push(newRule);
4972
+ } else if (isBwRuleTriple) {
4973
+ const [premise, conclusion] = liftBlankRuleVars(right, left);
4974
+ const headBlankLabels = collectBlankLabelsInTriples(conclusion);
4975
+ const newRule = new Rule(premise, conclusion, false, false, headBlankLabels);
4976
+
4977
+ const already = backRules.some(
4978
+ (rr) =>
4979
+ rr.isForward === newRule.isForward &&
4980
+ rr.isFuse === newRule.isFuse &&
4981
+ triplesListEqual(rr.premise, newRule.premise) &&
4982
+ triplesListEqual(rr.conclusion, newRule.conclusion),
4983
+ );
4984
+ if (!already) {
4985
+ backRules.push(newRule);
4986
+ indexBackRule(backRules, newRule);
5169
4987
  }
5170
4988
  }
5171
-
5172
- continue; // skip normal fact handling
5173
4989
  }
5174
4990
 
5175
- // Only skolemize blank nodes that occur explicitly in the rule head
5176
- const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter, fireKey, headSkolemCache);
4991
+ continue; // skip normal fact handling
4992
+ }
5177
4993
 
5178
- if (!isGroundTriple(inst)) continue;
5179
- if (hasFactIndexed(facts, inst)) continue;
4994
+ // Only skolemize blank nodes that occur explicitly in the rule head
4995
+ const inst = skolemizeTripleForHeadBlanks(instantiated, r.headBlankLabels, skMap, skCounter, fireKey, headSkolemCache);
5180
4996
 
5181
- factList.push(inst);
5182
- pushFactIndexed(facts, inst);
4997
+ if (!isGroundTriple(inst)) continue;
4998
+ if (hasFactIndexed(facts, inst)) continue;
5183
4999
 
5184
- derivedForward.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
5185
- changed = true;
5186
- }
5000
+ factList.push(inst);
5001
+ pushFactIndexed(facts, inst);
5002
+ derivedForward.push(new DerivedFact(inst, r, instantiatedPremises.slice(), { ...s }));
5003
+ changed = true;
5187
5004
  }
5188
5005
  }
5189
-
5190
- if (!changed) break;
5191
5006
  }
5192
5007
 
5193
- if (changed) anyChange = true;
5194
5008
  if (!changed) break;
5009
+ anyChange = true;
5195
5010
  }
5011
+
5196
5012
  return anyChange;
5197
5013
  }
5198
5014