eyeling 1.6.16 → 1.6.18

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 +367 -424
  2. package/package.json +1 -1
package/eyeling.js CHANGED
@@ -19,9 +19,9 @@
19
19
  const { version } = require('./package.json');
20
20
  const nodeCrypto = require('crypto');
21
21
 
22
- // ============================================================================
22
+ // ===========================================================================
23
23
  // Namespace constants
24
- // ============================================================================
24
+ // ===========================================================================
25
25
 
26
26
  const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
27
27
  const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
@@ -97,6 +97,47 @@ function normalizeDateTimeLex(s) {
97
97
  return t.trim();
98
98
  }
99
99
 
100
+ // ===========================================================================
101
+ // Run-level time helpers
102
+ // ===========================================================================
103
+
104
+ function localIsoDateTimeString(d) {
105
+ function pad(n, width = 2) {
106
+ return String(n).padStart(width, '0');
107
+ }
108
+ const year = d.getFullYear();
109
+ const month = d.getMonth() + 1;
110
+ const day = d.getDate();
111
+ const hour = d.getHours();
112
+ const min = d.getMinutes();
113
+ const sec = d.getSeconds();
114
+ const ms = d.getMilliseconds();
115
+ const offsetMin = -d.getTimezoneOffset(); // minutes east of UTC
116
+ const sign = offsetMin >= 0 ? '+' : '-';
117
+ const abs = Math.abs(offsetMin);
118
+ const oh = Math.floor(abs / 60);
119
+ const om = abs % 60;
120
+ const msPart = ms ? '.' + String(ms).padStart(3, '0') : '';
121
+ return (
122
+ pad(year, 4) +
123
+ '-' +
124
+ pad(month) +
125
+ '-' +
126
+ pad(day) +
127
+ 'T' +
128
+ pad(hour) +
129
+ ':' +
130
+ pad(min) +
131
+ ':' +
132
+ pad(sec) +
133
+ msPart +
134
+ sign +
135
+ pad(oh) +
136
+ ':' +
137
+ pad(om)
138
+ );
139
+ }
140
+
100
141
  function utcIsoDateTimeStringFromEpochSeconds(sec) {
101
142
  const ms = sec * 1000;
102
143
  const d = new Date(ms);
@@ -154,9 +195,9 @@ function deterministicSkolemIdFromKey(key) {
154
195
 
155
196
  let runLocalTimeCache = null;
156
197
 
157
- // ============================================================================
198
+ // ===========================================================================
158
199
  // AST (Abstract Syntax Tree)
159
- // ============================================================================
200
+ // ===========================================================================
160
201
 
161
202
  class Term {}
162
203
 
@@ -238,9 +279,9 @@ class DerivedFact {
238
279
  }
239
280
  }
240
281
 
241
- // ============================================================================
282
+ // ===========================================================================
242
283
  // LEXER
243
- // ============================================================================
284
+ // ===========================================================================
244
285
 
245
286
  class Token {
246
287
  constructor(typ, value = null) {
@@ -631,9 +672,9 @@ function lex(inputText) {
631
672
  return tokens;
632
673
  }
633
674
 
634
- // ============================================================================
675
+ // ===========================================================================
635
676
  // PREFIX ENVIRONMENT
636
- // ============================================================================
677
+ // ===========================================================================
637
678
 
638
679
  class PrefixEnv {
639
680
  constructor(map) {
@@ -789,9 +830,9 @@ function collectBlankLabelsInTriples(triples) {
789
830
  return acc;
790
831
  }
791
832
 
792
- // ============================================================================
833
+ // ===========================================================================
793
834
  // PARSER
794
- // ============================================================================
835
+ // ===========================================================================
795
836
 
796
837
  class Parser {
797
838
  constructor(tokens) {
@@ -1231,9 +1272,9 @@ class Parser {
1231
1272
  }
1232
1273
  }
1233
1274
 
1234
- // ============================================================================
1275
+ // ===========================================================================
1235
1276
  // Blank-node lifting and Skolemization
1236
- // ============================================================================
1277
+ // ===========================================================================
1237
1278
 
1238
1279
  function liftBlankRuleVars(premise, conclusion) {
1239
1280
  function convertTerm(t, mapping, counter) {
@@ -1340,9 +1381,9 @@ function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, f
1340
1381
  );
1341
1382
  }
1342
1383
 
1343
- // ============================================================================
1384
+ // ===========================================================================
1344
1385
  // Alpha equivalence helpers
1345
- // ============================================================================
1386
+ // ===========================================================================
1346
1387
 
1347
1388
  function termsEqual(a, b) {
1348
1389
  if (a === b) return true;
@@ -1614,9 +1655,9 @@ function hasAlphaEquiv(triples, tr) {
1614
1655
  return triples.some((t) => alphaEqTriple(t, tr));
1615
1656
  }
1616
1657
 
1617
- // ============================================================================
1658
+ // ===========================================================================
1618
1659
  // Indexes (facts + backward rules)
1619
- // ============================================================================
1660
+ // ===========================================================================
1620
1661
  //
1621
1662
  // Facts:
1622
1663
  // - __byPred: Map<predicateIRI, Triple[]>
@@ -1784,9 +1825,9 @@ function indexBackRule(backRules, r) {
1784
1825
  }
1785
1826
  }
1786
1827
 
1787
- // ============================================================================
1828
+ // ===========================================================================
1788
1829
  // Special predicate helpers
1789
- // ============================================================================
1830
+ // ===========================================================================
1790
1831
 
1791
1832
  function isRdfTypePred(p) {
1792
1833
  return p instanceof Iri && p.value === RDF_NS + 'type';
@@ -1804,9 +1845,9 @@ function isLogImpliedBy(p) {
1804
1845
  return p instanceof Iri && p.value === LOG_NS + 'impliedBy';
1805
1846
  }
1806
1847
 
1807
- // ============================================================================
1848
+ // ===========================================================================
1808
1849
  // Constraint / "test" builtins
1809
- // ============================================================================
1850
+ // ===========================================================================
1810
1851
 
1811
1852
  function isConstraintBuiltin(tr) {
1812
1853
  if (!(tr.p instanceof Iri)) return false;
@@ -1874,9 +1915,9 @@ function reorderPremiseForConstraints(premise) {
1874
1915
  return normal.concat(delayed);
1875
1916
  }
1876
1917
 
1877
- // ============================================================================
1918
+ // ===========================================================================
1878
1919
  // Unification + substitution
1879
- // ============================================================================
1920
+ // ===========================================================================
1880
1921
 
1881
1922
  function containsVarTerm(t, v) {
1882
1923
  if (t instanceof Var) return t.name === v;
@@ -1993,6 +2034,10 @@ function applySubstTriple(tr, s) {
1993
2034
  return new Triple(applySubstTerm(tr.s, s), applySubstTerm(tr.p, s), applySubstTerm(tr.o, s));
1994
2035
  }
1995
2036
 
2037
+ function iriValue(t) {
2038
+ return t instanceof Iri ? t.value : null;
2039
+ }
2040
+
1996
2041
  function unifyOpenWithList(prefix, tailv, ys, subst) {
1997
2042
  if (ys.length < prefix.length) return null;
1998
2043
  let s2 = { ...subst };
@@ -2184,9 +2229,9 @@ function composeSubst(outer, delta) {
2184
2229
  return out;
2185
2230
  }
2186
2231
 
2187
- // ============================================================================
2232
+ // ===========================================================================
2188
2233
  // BUILTINS
2189
- // ============================================================================
2234
+ // ===========================================================================
2190
2235
 
2191
2236
  function literalParts(lit) {
2192
2237
  // Split a literal into lexical form and datatype IRI (if any).
@@ -2524,6 +2569,10 @@ function parseXsdFloatSpecialLex(s) {
2524
2569
  return null;
2525
2570
  }
2526
2571
 
2572
+ // ===========================================================================
2573
+ // Math builtin helpers
2574
+ // ===========================================================================
2575
+
2527
2576
  function formatXsdFloatSpecialLex(n) {
2528
2577
  if (n === Infinity) return 'INF';
2529
2578
  if (n === -Infinity) return '-INF';
@@ -2678,6 +2727,10 @@ function pow10n(k) {
2678
2727
  return 10n ** BigInt(k);
2679
2728
  }
2680
2729
 
2730
+ // ===========================================================================
2731
+ // Time & duration builtin helpers
2732
+ // ===========================================================================
2733
+
2681
2734
  function parseXsdDateTerm(t) {
2682
2735
  if (!(t instanceof Literal)) return null;
2683
2736
  const [lex, dt] = literalParts(t.value);
@@ -2759,7 +2812,7 @@ function parseNumericForCompareTerm(t) {
2759
2812
  }
2760
2813
 
2761
2814
  function cmpNumericInfo(aInfo, bInfo, op) {
2762
- // op is one of ">", "<", ">=", "<="
2815
+ // op is one of ">", "<", ">=", "<=", "==", "!="
2763
2816
  if (!aInfo || !bInfo) return false;
2764
2817
 
2765
2818
  if (aInfo.kind === 'bigint' && bInfo.kind === 'bigint') {
@@ -2784,6 +2837,19 @@ function cmpNumericInfo(aInfo, bInfo, op) {
2784
2837
  return false;
2785
2838
  }
2786
2839
 
2840
+ function evalNumericComparisonBuiltin(g, subst, op) {
2841
+ const aInfo = parseNumericForCompareTerm(g.s);
2842
+ const bInfo = parseNumericForCompareTerm(g.o);
2843
+ if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, op)) return [{ ...subst }];
2844
+
2845
+ if (g.s instanceof ListTerm && g.s.elems.length === 2) {
2846
+ const a2 = parseNumericForCompareTerm(g.s.elems[0]);
2847
+ const b2 = parseNumericForCompareTerm(g.s.elems[1]);
2848
+ if (a2 && b2 && cmpNumericInfo(a2, b2, op)) return [{ ...subst }];
2849
+ }
2850
+ return [];
2851
+ }
2852
+
2787
2853
  function parseNumOrDuration(t) {
2788
2854
  const n = parseNum(t);
2789
2855
  if (n !== null) return n;
@@ -2816,24 +2882,6 @@ function formatDurationLiteralFromSeconds(secs) {
2816
2882
  const literalLex = neg ? `"-P${days}D"` : `"P${days}D"`;
2817
2883
  return new Literal(`${literalLex}^^<${XSD_NS}duration>`);
2818
2884
  }
2819
-
2820
- function listAppendSplit(parts, resElems, subst) {
2821
- if (!parts.length) {
2822
- if (!resElems.length) return [{ ...subst }];
2823
- return [];
2824
- }
2825
- const out = [];
2826
- const n = resElems.length;
2827
- for (let k = 0; k <= n; k++) {
2828
- const left = new ListTerm(resElems.slice(0, k));
2829
- let s1 = unifyTermListAppend(parts[0], left, subst);
2830
- if (s1 === null) continue;
2831
- const restElems = resElems.slice(k);
2832
- out.push(...listAppendSplit(parts.slice(1), restElems, s1));
2833
- }
2834
- return out;
2835
- }
2836
-
2837
2885
  function numEqualTerm(t, n, eps = 1e-9) {
2838
2886
  const v = parseNum(t);
2839
2887
  if (v === null) return false;
@@ -3010,6 +3058,27 @@ function evalUnaryMathRel(g, subst, forwardFn, inverseFn /* may be null */) {
3010
3058
  return [];
3011
3059
  }
3012
3060
 
3061
+ // ===========================================================================
3062
+ // List builtin helpers
3063
+ // ===========================================================================
3064
+
3065
+ function listAppendSplit(parts, resElems, subst) {
3066
+ if (!parts.length) {
3067
+ if (!resElems.length) return [{ ...subst }];
3068
+ return [];
3069
+ }
3070
+ const out = [];
3071
+ const n = resElems.length;
3072
+ for (let k = 0; k <= n; k++) {
3073
+ const left = new ListTerm(resElems.slice(0, k));
3074
+ let s1 = unifyTermListAppend(parts[0], left, subst);
3075
+ if (s1 === null) continue;
3076
+ const restElems = resElems.slice(k);
3077
+ out.push(...listAppendSplit(parts.slice(1), restElems, s1));
3078
+ }
3079
+ return out;
3080
+ }
3081
+
3013
3082
  function evalListFirstLikeBuiltin(sTerm, oTerm, subst) {
3014
3083
  if (!(sTerm instanceof ListTerm)) return [];
3015
3084
  if (!sTerm.elems.length) return [];
@@ -3042,175 +3111,219 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
3042
3111
  return [];
3043
3112
  }
3044
3113
 
3045
- // ============================================================================
3046
- // Backward proof & builtins mutual recursion — declarations first
3047
- // ============================================================================
3114
+ // ===========================================================================
3115
+ // RDF list materialization
3116
+ // ===========================================================================
3048
3117
 
3049
- function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3050
- const g = applySubstTriple(goal, subst);
3118
+ // Turn RDF Collections described with rdf:first/rdf:rest (+ rdf:nil) into ListTerm terms.
3119
+ // This mutates triples/rules in-place so list:* builtins work on RDF-serialized lists too.
3120
+ function materializeRdfLists(triples, forwardRules, backwardRules) {
3121
+ const RDF_FIRST = RDF_NS + 'first';
3122
+ const RDF_REST = RDF_NS + 'rest';
3123
+ const RDF_NIL = RDF_NS + 'nil';
3051
3124
 
3052
- function hashLiteral(t, algo) {
3053
- // Accept only literals, interpret lexical form as UTF-8 string
3054
- if (!(t instanceof Literal)) return null;
3055
- const [lex, _dt] = literalParts(t.value);
3056
- const input = stripQuotes(lex);
3057
- try {
3058
- const digest = nodeCrypto.createHash(algo).update(input, 'utf8').digest('hex');
3059
- // plain string literal with the hex digest
3060
- return new Literal(JSON.stringify(digest));
3061
- } catch (e) {
3062
- return null;
3063
- }
3125
+ function nodeKey(t) {
3126
+ if (t instanceof Blank) return 'B:' + t.label;
3127
+ if (t instanceof Iri) return 'I:' + t.value;
3128
+ return null;
3064
3129
  }
3065
3130
 
3066
- // -----------------------------------------------------------------
3067
- // 4.1 crypto: builtins
3068
- // -----------------------------------------------------------------
3131
+ // Collect first/rest arcs from *input triples*
3132
+ const firstMap = new Map(); // key(subject) -> Term (object)
3133
+ const restMap = new Map(); // key(subject) -> Term (object)
3134
+ for (const tr of triples) {
3135
+ if (!(tr.p instanceof Iri)) continue;
3136
+ const k = nodeKey(tr.s);
3137
+ if (!k) continue;
3138
+ if (tr.p.value === RDF_FIRST) firstMap.set(k, tr.o);
3139
+ else if (tr.p.value === RDF_REST) restMap.set(k, tr.o);
3140
+ }
3141
+ if (!firstMap.size && !restMap.size) return;
3069
3142
 
3070
- // crypto:sha
3071
- // true iff ?o is the SHA-1 hash of the subject string.
3072
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'sha') {
3073
- const lit = hashLiteral(g.s, 'sha1');
3074
- if (!lit) return [];
3075
- if (g.o instanceof Var) {
3076
- const s2 = { ...subst };
3077
- s2[g.o.name] = lit;
3078
- return [s2];
3143
+ const cache = new Map(); // key(node) -> ListTerm
3144
+ const visiting = new Set(); // cycle guard
3145
+
3146
+ function buildListForKey(k) {
3147
+ if (cache.has(k)) return cache.get(k);
3148
+ if (visiting.has(k)) return null; // cycle => not a well-formed list
3149
+ visiting.add(k);
3150
+
3151
+ // rdf:nil => ()
3152
+ if (k === 'I:' + RDF_NIL) {
3153
+ const empty = new ListTerm([]);
3154
+ cache.set(k, empty);
3155
+ visiting.delete(k);
3156
+ return empty;
3079
3157
  }
3080
- const s2 = unifyTerm(g.o, lit, subst);
3081
- return s2 !== null ? [s2] : [];
3082
- }
3083
3158
 
3084
- // crypto:md5
3085
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'md5') {
3086
- const lit = hashLiteral(g.s, 'md5');
3087
- if (!lit) return [];
3088
- if (g.o instanceof Var) {
3089
- const s2 = { ...subst };
3090
- s2[g.o.name] = lit;
3091
- return [s2];
3159
+ const head = firstMap.get(k);
3160
+ const tail = restMap.get(k);
3161
+ if (head === undefined || tail === undefined) {
3162
+ visiting.delete(k);
3163
+ return null; // not a full cons cell
3092
3164
  }
3093
- const s2 = unifyTerm(g.o, lit, subst);
3094
- return s2 !== null ? [s2] : [];
3095
- }
3096
3165
 
3097
- // crypto:sha256
3098
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'sha256') {
3099
- const lit = hashLiteral(g.s, 'sha256');
3100
- if (!lit) return [];
3101
- if (g.o instanceof Var) {
3102
- const s2 = { ...subst };
3103
- s2[g.o.name] = lit;
3104
- return [s2];
3166
+ const headTerm = rewriteTerm(head);
3167
+
3168
+ let tailListElems = null;
3169
+ if (tail instanceof Iri && tail.value === RDF_NIL) {
3170
+ tailListElems = [];
3171
+ } else {
3172
+ const tk = nodeKey(tail);
3173
+ if (!tk) {
3174
+ visiting.delete(k);
3175
+ return null;
3176
+ }
3177
+ const tailList = buildListForKey(tk);
3178
+ if (!tailList) {
3179
+ visiting.delete(k);
3180
+ return null;
3181
+ }
3182
+ tailListElems = tailList.elems;
3105
3183
  }
3106
- const s2 = unifyTerm(g.o, lit, subst);
3107
- return s2 !== null ? [s2] : [];
3184
+
3185
+ const out = new ListTerm([headTerm, ...tailListElems]);
3186
+ cache.set(k, out);
3187
+ visiting.delete(k);
3188
+ return out;
3108
3189
  }
3109
3190
 
3110
- // crypto:sha512
3111
- if (g.p instanceof Iri && g.p.value === CRYPTO_NS + 'sha512') {
3112
- const lit = hashLiteral(g.s, 'sha512');
3113
- if (!lit) return [];
3114
- if (g.o instanceof Var) {
3115
- const s2 = { ...subst };
3116
- s2[g.o.name] = lit;
3117
- return [s2];
3191
+ function rewriteTerm(t) {
3192
+ // Replace list nodes (Blank/Iri) by their constructed ListTerm when possible
3193
+ const k = nodeKey(t);
3194
+ if (k) {
3195
+ const built = buildListForKey(k);
3196
+ if (built) return built;
3197
+ // Also rewrite rdf:nil even if not otherwise referenced
3198
+ if (t instanceof Iri && t.value === RDF_NIL) return new ListTerm([]);
3199
+ return t;
3118
3200
  }
3119
- const s2 = unifyTerm(g.o, lit, subst);
3120
- return s2 !== null ? [s2] : [];
3201
+ if (t instanceof ListTerm) {
3202
+ let changed = false;
3203
+ const elems = t.elems.map((e) => {
3204
+ const r = rewriteTerm(e);
3205
+ if (r !== e) changed = true;
3206
+ return r;
3207
+ });
3208
+ return changed ? new ListTerm(elems) : t;
3209
+ }
3210
+ if (t instanceof OpenListTerm) {
3211
+ let changed = false;
3212
+ const prefix = t.prefix.map((e) => {
3213
+ const r = rewriteTerm(e);
3214
+ if (r !== e) changed = true;
3215
+ return r;
3216
+ });
3217
+ return changed ? new OpenListTerm(prefix, t.tailVar) : t;
3218
+ }
3219
+ if (t instanceof FormulaTerm) {
3220
+ for (const tr of t.triples) rewriteTriple(tr);
3221
+ return t;
3222
+ }
3223
+ return t;
3121
3224
  }
3122
3225
 
3123
- // -----------------------------------------------------------------
3124
- // 4.2 math: builtins
3125
- // -----------------------------------------------------------------
3226
+ function rewriteTriple(tr) {
3227
+ tr.s = rewriteTerm(tr.s);
3228
+ tr.p = rewriteTerm(tr.p);
3229
+ tr.o = rewriteTerm(tr.o);
3230
+ }
3126
3231
 
3127
- // math:greaterThan
3128
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'greaterThan') {
3129
- const aInfo = parseNumericForCompareTerm(g.s);
3130
- const bInfo = parseNumericForCompareTerm(g.o);
3131
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '>')) return [{ ...subst }];
3232
+ // Pre-build all reachable list heads
3233
+ for (const k of firstMap.keys()) buildListForKey(k);
3132
3234
 
3133
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3134
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3135
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3136
- if (a2 && b2 && cmpNumericInfo(a2, b2, '>')) return [{ ...subst }];
3137
- }
3138
- return [];
3235
+ // Rewrite input triples + rules in-place
3236
+ for (const tr of triples) rewriteTriple(tr);
3237
+ for (const r of forwardRules) {
3238
+ for (const tr of r.premise) rewriteTriple(tr);
3239
+ for (const tr of r.conclusion) rewriteTriple(tr);
3240
+ }
3241
+ for (const r of backwardRules) {
3242
+ for (const tr of r.premise) rewriteTriple(tr);
3243
+ for (const tr of r.conclusion) rewriteTriple(tr);
3139
3244
  }
3245
+ }
3140
3246
 
3141
- // math:lessThan
3142
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'lessThan') {
3143
- const aInfo = parseNumericForCompareTerm(g.s);
3144
- const bInfo = parseNumericForCompareTerm(g.o);
3145
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '<')) return [{ ...subst }];
3247
+ // ===========================================================================
3248
+ // Crypto builtin helpers
3249
+ // ===========================================================================
3146
3250
 
3147
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3148
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3149
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3150
- if (a2 && b2 && cmpNumericInfo(a2, b2, '<')) return [{ ...subst }];
3151
- }
3152
- return [];
3251
+ function hashLiteralTerm(t, algo) {
3252
+ if (!(t instanceof Literal)) return null;
3253
+ const [lex] = literalParts(t.value);
3254
+ const input = stripQuotes(lex);
3255
+ try {
3256
+ const digest = nodeCrypto.createHash(algo).update(input, 'utf8').digest('hex');
3257
+ return new Literal(JSON.stringify(digest));
3258
+ } catch (e) {
3259
+ return null;
3153
3260
  }
3261
+ }
3154
3262
 
3155
- // math:notLessThan
3156
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'notLessThan') {
3157
- const aInfo = parseNumericForCompareTerm(g.s);
3158
- const bInfo = parseNumericForCompareTerm(g.o);
3159
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '>=')) return [{ ...subst }];
3160
-
3161
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3162
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3163
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3164
- if (a2 && b2 && cmpNumericInfo(a2, b2, '>=')) return [{ ...subst }];
3165
- }
3166
- return [];
3263
+ function evalCryptoHashBuiltin(g, subst, algo) {
3264
+ const lit = hashLiteralTerm(g.s, algo);
3265
+ if (!lit) return [];
3266
+ if (g.o instanceof Var) {
3267
+ const s2 = { ...subst };
3268
+ s2[g.o.name] = lit;
3269
+ return [s2];
3167
3270
  }
3271
+ const s2 = unifyTerm(g.o, lit, subst);
3272
+ return s2 !== null ? [s2] : [];
3273
+ }
3168
3274
 
3169
- // math:notGreaterThan
3170
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'notGreaterThan') {
3171
- const aInfo = parseNumericForCompareTerm(g.s);
3172
- const bInfo = parseNumericForCompareTerm(g.o);
3173
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '<=')) return [{ ...subst }];
3275
+ // ===========================================================================
3276
+ // Builtin evaluation
3277
+ // ===========================================================================
3278
+ // Backward proof & builtins mutual recursion — declarations first
3174
3279
 
3175
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3176
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3177
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3178
- if (a2 && b2 && cmpNumericInfo(a2, b2, '<=')) return [{ ...subst }];
3179
- }
3180
- return [];
3181
- }
3280
+ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3281
+ const g = applySubstTriple(goal, subst);
3282
+ const pv = iriValue(g.p);
3283
+ if (pv === null) return null;
3182
3284
 
3183
- // math:equalTo
3184
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'equalTo') {
3185
- const aInfo = parseNumericForCompareTerm(g.s);
3186
- const bInfo = parseNumericForCompareTerm(g.o);
3187
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '==')) return [{ ...subst }];
3285
+ // -----------------------------------------------------------------
3286
+ // 4.1 crypto: builtins
3287
+ // -----------------------------------------------------------------
3188
3288
 
3189
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3190
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3191
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3192
- if (a2 && b2 && cmpNumericInfo(a2, b2, '==')) return [{ ...subst }];
3193
- }
3194
- return [];
3195
- }
3289
+ // crypto:sha, crypto:md5, crypto:sha256, crypto:sha512
3290
+ // Digest builtins. crypto:sha uses SHA-1 per the N3/crypto convention.
3291
+ const cryptoAlgo =
3292
+ pv === CRYPTO_NS + 'sha'
3293
+ ? 'sha1'
3294
+ : pv === CRYPTO_NS + 'md5'
3295
+ ? 'md5'
3296
+ : pv === CRYPTO_NS + 'sha256'
3297
+ ? 'sha256'
3298
+ : pv === CRYPTO_NS + 'sha512'
3299
+ ? 'sha512'
3300
+ : null;
3301
+ if (cryptoAlgo) return evalCryptoHashBuiltin(g, subst, cryptoAlgo);
3196
3302
 
3197
- // math:notEqualTo
3198
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'notEqualTo') {
3199
- const aInfo = parseNumericForCompareTerm(g.s);
3200
- const bInfo = parseNumericForCompareTerm(g.o);
3201
- if (aInfo && bInfo && cmpNumericInfo(aInfo, bInfo, '!=')) return [{ ...subst }];
3303
+ // -----------------------------------------------------------------
3304
+ // 4.2 math: builtins
3305
+ // -----------------------------------------------------------------
3202
3306
 
3203
- if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3204
- const a2 = parseNumericForCompareTerm(g.s.elems[0]);
3205
- const b2 = parseNumericForCompareTerm(g.s.elems[1]);
3206
- if (a2 && b2 && cmpNumericInfo(a2, b2, '!=')) return [{ ...subst }];
3207
- }
3208
- return [];
3209
- }
3307
+ // math:greaterThan / lessThan / notLessThan / notGreaterThan / equalTo / notEqualTo
3308
+ const mathCmpOp =
3309
+ pv === MATH_NS + 'greaterThan'
3310
+ ? '>'
3311
+ : pv === MATH_NS + 'lessThan'
3312
+ ? '<'
3313
+ : pv === MATH_NS + 'notLessThan'
3314
+ ? '>='
3315
+ : pv === MATH_NS + 'notGreaterThan'
3316
+ ? '<='
3317
+ : pv === MATH_NS + 'equalTo'
3318
+ ? '=='
3319
+ : pv === MATH_NS + 'notEqualTo'
3320
+ ? '!='
3321
+ : null;
3322
+ if (mathCmpOp) return evalNumericComparisonBuiltin(g, subst, mathCmpOp);
3210
3323
 
3211
3324
  // math:sum
3212
3325
  // Schema: ( $s.i+ )+ math:sum $o-
3213
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sum') {
3326
+ if (pv === MATH_NS + 'sum') {
3214
3327
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 2) return [];
3215
3328
  const xs = g.s.elems;
3216
3329
 
@@ -3264,7 +3377,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3264
3377
 
3265
3378
  // math:product
3266
3379
  // Schema: ( $s.i+ )+ math:product $o-
3267
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'product') {
3380
+ if (pv === MATH_NS + 'product') {
3268
3381
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 2) return [];
3269
3382
  const xs = g.s.elems;
3270
3383
 
@@ -3316,7 +3429,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3316
3429
 
3317
3430
  // math:difference
3318
3431
  // Schema: ( $s.1+ $s.2+ )+ math:difference $o-
3319
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'difference') {
3432
+ if (pv === MATH_NS + 'difference') {
3320
3433
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3321
3434
  const [a0, b0] = g.s.elems;
3322
3435
 
@@ -3404,7 +3517,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3404
3517
 
3405
3518
  // math:quotient
3406
3519
  // Schema: ( $s.1+ $s.2+ )+ math:quotient $o-
3407
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'quotient') {
3520
+ if (pv === MATH_NS + 'quotient') {
3408
3521
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3409
3522
  const [a0, b0] = g.s.elems;
3410
3523
 
@@ -3433,7 +3546,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3433
3546
  // math:integerQuotient
3434
3547
  // Schema: ( $a $b ) math:integerQuotient $q
3435
3548
  // Cwm: divide first integer by second integer, ignoring remainder. :contentReference[oaicite:1]{index=1}
3436
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'integerQuotient') {
3549
+ if (pv === MATH_NS + 'integerQuotient') {
3437
3550
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3438
3551
  const [a0, b0] = g.s.elems;
3439
3552
 
@@ -3487,7 +3600,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3487
3600
  }
3488
3601
 
3489
3602
  // math:exponentiation
3490
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'exponentiation') {
3603
+ if (pv === MATH_NS + 'exponentiation') {
3491
3604
  if (g.s instanceof ListTerm && g.s.elems.length === 2) {
3492
3605
  const baseTerm = g.s.elems[0];
3493
3606
  const expTerm = g.s.elems[1];
@@ -3534,7 +3647,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3534
3647
  }
3535
3648
 
3536
3649
  // math:absoluteValue
3537
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'absoluteValue') {
3650
+ if (pv === MATH_NS + 'absoluteValue') {
3538
3651
  const a = parseNum(g.s);
3539
3652
  if (a === null) return [];
3540
3653
 
@@ -3555,58 +3668,58 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3555
3668
  }
3556
3669
 
3557
3670
  // math:acos
3558
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'acos') {
3671
+ if (pv === MATH_NS + 'acos') {
3559
3672
  return evalUnaryMathRel(g, subst, Math.acos, Math.cos);
3560
3673
  }
3561
3674
 
3562
3675
  // math:asin
3563
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'asin') {
3676
+ if (pv === MATH_NS + 'asin') {
3564
3677
  return evalUnaryMathRel(g, subst, Math.asin, Math.sin);
3565
3678
  }
3566
3679
 
3567
3680
  // math:atan
3568
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'atan') {
3681
+ if (pv === MATH_NS + 'atan') {
3569
3682
  return evalUnaryMathRel(g, subst, Math.atan, Math.tan);
3570
3683
  }
3571
3684
 
3572
3685
  // math:sin (inverse uses principal asin)
3573
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sin') {
3686
+ if (pv === MATH_NS + 'sin') {
3574
3687
  return evalUnaryMathRel(g, subst, Math.sin, Math.asin);
3575
3688
  }
3576
3689
 
3577
3690
  // math:cos (inverse uses principal acos)
3578
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'cos') {
3691
+ if (pv === MATH_NS + 'cos') {
3579
3692
  return evalUnaryMathRel(g, subst, Math.cos, Math.acos);
3580
3693
  }
3581
3694
 
3582
3695
  // math:tan (inverse uses principal atan)
3583
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tan') {
3696
+ if (pv === MATH_NS + 'tan') {
3584
3697
  return evalUnaryMathRel(g, subst, Math.tan, Math.atan);
3585
3698
  }
3586
3699
 
3587
3700
  // math:sinh / cosh / tanh (guard for JS availability)
3588
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sinh') {
3701
+ if (pv === MATH_NS + 'sinh') {
3589
3702
  if (typeof Math.sinh !== 'function' || typeof Math.asinh !== 'function') return [];
3590
3703
  return evalUnaryMathRel(g, subst, Math.sinh, Math.asinh);
3591
3704
  }
3592
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'cosh') {
3705
+ if (pv === MATH_NS + 'cosh') {
3593
3706
  if (typeof Math.cosh !== 'function' || typeof Math.acosh !== 'function') return [];
3594
3707
  return evalUnaryMathRel(g, subst, Math.cosh, Math.acosh);
3595
3708
  }
3596
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tanh') {
3709
+ if (pv === MATH_NS + 'tanh') {
3597
3710
  if (typeof Math.tanh !== 'function' || typeof Math.atanh !== 'function') return [];
3598
3711
  return evalUnaryMathRel(g, subst, Math.tanh, Math.atanh);
3599
3712
  }
3600
3713
 
3601
3714
  // math:degrees (inverse is radians)
3602
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'degrees') {
3715
+ if (pv === MATH_NS + 'degrees') {
3603
3716
  const toDeg = (rad) => (rad * 180.0) / Math.PI;
3604
3717
  const toRad = (deg) => (deg * Math.PI) / 180.0;
3605
3718
  return evalUnaryMathRel(g, subst, toDeg, toRad);
3606
3719
  }
3607
3720
 
3608
3721
  // math:negation (inverse is itself)
3609
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'negation') {
3722
+ if (pv === MATH_NS + 'negation') {
3610
3723
  const neg = (x) => -x;
3611
3724
  return evalUnaryMathRel(g, subst, neg, neg);
3612
3725
  }
@@ -3614,7 +3727,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3614
3727
  // math:remainder
3615
3728
  // Subject is a list (dividend divisor); object is the remainder.
3616
3729
  // Schema: ( $a $b ) math:remainder $r
3617
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'remainder') {
3730
+ if (pv === MATH_NS + 'remainder') {
3618
3731
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3619
3732
  const [a0, b0] = g.s.elems;
3620
3733
 
@@ -3664,7 +3777,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3664
3777
  // If there are two such numbers, then the one closest to positive infinity is returned.
3665
3778
  // Schema: $s+ math:rounded $o-
3666
3779
  // Note: spec says $o is xsd:integer, but we also accept any numeric $o that equals the rounded value.
3667
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'rounded') {
3780
+ if (pv === MATH_NS + 'rounded') {
3668
3781
  const a = parseNum(g.s);
3669
3782
  if (a === null) return [];
3670
3783
  if (Number.isNaN(a)) return [];
@@ -3693,7 +3806,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3693
3806
 
3694
3807
  // time:localTime
3695
3808
  // "" time:localTime ?D. binds ?D to “now” as xsd:dateTime.
3696
- if (g.p instanceof Iri && g.p.value === TIME_NS + 'localTime') {
3809
+ if (pv === TIME_NS + 'localTime') {
3697
3810
  const now = getNowLex();
3698
3811
 
3699
3812
  if (g.o instanceof Var) {
@@ -3715,7 +3828,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3715
3828
  // list:append
3716
3829
  // true if and only if $o is the concatenation of all lists $s.i.
3717
3830
  // Schema: ( $s.i?[*] )+ list:append $o?
3718
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'append') {
3831
+ if (pv === LIST_NS + 'append') {
3719
3832
  if (!(g.s instanceof ListTerm)) return [];
3720
3833
  const parts = g.s.elems;
3721
3834
  if (g.o instanceof ListTerm) {
@@ -3739,14 +3852,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3739
3852
  // list:first and rdf:first
3740
3853
  // true iff $s is a list and $o is the first member of that list.
3741
3854
  // Schema: $s+ list:first $o-
3742
- if (g.p instanceof Iri && (g.p.value === LIST_NS + 'first' || g.p.value === RDF_NS + 'first')) {
3855
+ if (pv === LIST_NS + 'first' || pv === RDF_NS + 'first') {
3743
3856
  return evalListFirstLikeBuiltin(g.s, g.o, subst);
3744
3857
  }
3745
3858
 
3746
3859
  // list:rest and rdf:rest
3747
3860
  // true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
3748
3861
  // Schema: $s+ list:rest $o-
3749
- if (g.p instanceof Iri && (g.p.value === LIST_NS + 'rest' || g.p.value === RDF_NS + 'rest')) {
3862
+ if (pv === LIST_NS + 'rest' || pv === RDF_NS + 'rest') {
3750
3863
  return evalListRestLikeBuiltin(g.s, g.o, subst);
3751
3864
  }
3752
3865
 
@@ -3754,7 +3867,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3754
3867
  // Multi-solution builtin:
3755
3868
  // For a list subject $s, generate solutions by unifying $o with (index value).
3756
3869
  // This allows $o to be a variable (e.g., ?Y) or a pattern (e.g., (?i "Dewey")).
3757
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'iterate') {
3870
+ if (pv === LIST_NS + 'iterate') {
3758
3871
  if (!(g.s instanceof ListTerm)) return [];
3759
3872
  const xs = g.s.elems;
3760
3873
  const outs = [];
@@ -3795,7 +3908,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3795
3908
  // list:last
3796
3909
  // true iff $s is a list and $o is the last member of that list.
3797
3910
  // Schema: $s+ list:last $o-
3798
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'last') {
3911
+ if (pv === LIST_NS + 'last') {
3799
3912
  if (!(g.s instanceof ListTerm)) return [];
3800
3913
  const xs = g.s.elems;
3801
3914
  if (!xs.length) return [];
@@ -3807,7 +3920,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3807
3920
  // list:memberAt
3808
3921
  // true iff $s.1 is a list, $s.2 is a valid index, and $o is the member at that index.
3809
3922
  // Schema: ( $s.1+ $s.2?[*] )+ list:memberAt $o?[*]
3810
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'memberAt') {
3923
+ if (pv === LIST_NS + 'memberAt') {
3811
3924
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3812
3925
  const [listTerm, indexTerm] = g.s.elems;
3813
3926
  if (!(listTerm instanceof ListTerm)) return [];
@@ -3846,7 +3959,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3846
3959
  // list:remove
3847
3960
  // true iff $s.1 is a list and $o is that list with all occurrences of $s.2 removed.
3848
3961
  // Schema: ( $s.1+ $s.2+ )+ list:remove $o-
3849
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'remove') {
3962
+ if (pv === LIST_NS + 'remove') {
3850
3963
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3851
3964
  const [listTerm, itemTerm] = g.s.elems;
3852
3965
  if (!(listTerm instanceof ListTerm)) return [];
@@ -3868,7 +3981,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3868
3981
  }
3869
3982
 
3870
3983
  // list:member
3871
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'member') {
3984
+ if (pv === LIST_NS + 'member') {
3872
3985
  if (!(g.s instanceof ListTerm)) return [];
3873
3986
  const outs = [];
3874
3987
  for (const x of g.s.elems) {
@@ -3879,7 +3992,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3879
3992
  }
3880
3993
 
3881
3994
  // list:in
3882
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'in') {
3995
+ if (pv === LIST_NS + 'in') {
3883
3996
  if (!(g.o instanceof ListTerm)) return [];
3884
3997
  const outs = [];
3885
3998
  for (const x of g.o.elems) {
@@ -3890,7 +4003,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3890
4003
  }
3891
4004
 
3892
4005
  // list:length (strict: do not accept integer<->decimal matches for a ground object)
3893
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'length') {
4006
+ if (pv === LIST_NS + 'length') {
3894
4007
  if (!(g.s instanceof ListTerm)) return [];
3895
4008
  const nTerm = new Literal(String(g.s.elems.length));
3896
4009
 
@@ -3904,7 +4017,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3904
4017
  }
3905
4018
 
3906
4019
  // list:notMember
3907
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'notMember') {
4020
+ if (pv === LIST_NS + 'notMember') {
3908
4021
  if (!(g.s instanceof ListTerm)) return [];
3909
4022
  for (const el of g.s.elems) {
3910
4023
  if (unifyTerm(g.o, el, subst) !== null) return [];
@@ -3913,7 +4026,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3913
4026
  }
3914
4027
 
3915
4028
  // list:reverse
3916
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'reverse') {
4029
+ if (pv === LIST_NS + 'reverse') {
3917
4030
  if (g.s instanceof ListTerm) {
3918
4031
  const rev = [...g.s.elems].reverse();
3919
4032
  const rterm = new ListTerm(rev);
@@ -3930,7 +4043,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3930
4043
  }
3931
4044
 
3932
4045
  // list:sort
3933
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'sort') {
4046
+ if (pv === LIST_NS + 'sort') {
3934
4047
  function cmpTermForSort(a, b) {
3935
4048
  if (a instanceof Literal && b instanceof Literal) {
3936
4049
  const [lexA] = literalParts(a.value);
@@ -3998,7 +4111,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3998
4111
  }
3999
4112
 
4000
4113
  // list:map
4001
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'map') {
4114
+ if (pv === LIST_NS + 'map') {
4002
4115
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4003
4116
  const [inputTerm, predTerm] = g.s.elems;
4004
4117
  if (!(inputTerm instanceof ListTerm)) return [];
@@ -4024,7 +4137,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4024
4137
  }
4025
4138
 
4026
4139
  // list:firstRest
4027
- if (g.p instanceof Iri && g.p.value === LIST_NS + 'firstRest') {
4140
+ if (pv === LIST_NS + 'firstRest') {
4028
4141
  if (g.s instanceof ListTerm) {
4029
4142
  if (!g.s.elems.length) return [];
4030
4143
  const first = g.s.elems[0];
@@ -4062,13 +4175,13 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4062
4175
  // -----------------------------------------------------------------
4063
4176
 
4064
4177
  // log:equalTo
4065
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'equalTo') {
4178
+ if (pv === LOG_NS + 'equalTo') {
4066
4179
  const s2 = unifyTerm(goal.s, goal.o, subst);
4067
4180
  return s2 !== null ? [s2] : [];
4068
4181
  }
4069
4182
 
4070
4183
  // log:notEqualTo
4071
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'notEqualTo') {
4184
+ if (pv === LOG_NS + 'notEqualTo') {
4072
4185
  const s2 = unifyTerm(goal.s, goal.o, subst);
4073
4186
  if (s2 !== null) return [];
4074
4187
  return [{ ...subst }];
@@ -4077,7 +4190,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4077
4190
  // log:dtlit
4078
4191
  // Schema: ( $s.1? $s.2? )? log:dtlit $o?
4079
4192
  // true iff $o is a datatyped literal with string value $s.1 and datatype IRI $s.2
4080
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'dtlit') {
4193
+ if (pv === LOG_NS + 'dtlit') {
4081
4194
  // Fully unbound (both arguments '?'-mode): treat as satisfiable, succeed once.
4082
4195
  // Required by notation3tests "success-fullUnbound-*".
4083
4196
  if (g.s instanceof Var && g.o instanceof Var) return [{ ...subst }];
@@ -4130,7 +4243,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4130
4243
  // log:langlit
4131
4244
  // Schema: ( $s.1? $s.2? )? log:langlit $o?
4132
4245
  // true iff $o is a language-tagged literal with string value $s.1 and language tag $s.2
4133
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'langlit') {
4246
+ if (pv === LOG_NS + 'langlit') {
4134
4247
  // Fully unbound (both arguments '?'-mode): treat as satisfiable, succeed once.
4135
4248
  if (g.s instanceof Var && g.o instanceof Var) return [{ ...subst }];
4136
4249
  const results = [];
@@ -4184,7 +4297,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4184
4297
  }
4185
4298
 
4186
4299
  // log:implies — expose internal forward rules as data
4187
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'implies') {
4300
+ if (pv === LOG_NS + 'implies') {
4188
4301
  const allFw = backRules.__allForwardRules || [];
4189
4302
  const results = [];
4190
4303
 
@@ -4212,7 +4325,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4212
4325
  }
4213
4326
 
4214
4327
  // log:impliedBy — expose internal backward rules as data
4215
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'impliedBy') {
4328
+ if (pv === LOG_NS + 'impliedBy') {
4216
4329
  const allBw = backRules.__allBackwardRules || backRules;
4217
4330
  const results = [];
4218
4331
 
@@ -4242,7 +4355,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4242
4355
 
4243
4356
  // log:notIncludes (EYE-style: "not provable in scope")
4244
4357
  // Delay until we have a frozen scope snapshot to avoid early success.
4245
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'notIncludes') {
4358
+ if (pv === LOG_NS + 'notIncludes') {
4246
4359
  if (!(g.o instanceof FormulaTerm)) return [];
4247
4360
 
4248
4361
  let scopeFacts = null;
@@ -4265,7 +4378,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4265
4378
  }
4266
4379
 
4267
4380
  // log:collectAllIn (scoped)
4268
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'collectAllIn') {
4381
+ if (pv === LOG_NS + 'collectAllIn') {
4269
4382
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
4270
4383
  const [valueTempl, clauseTerm, listTerm] = g.s.elems;
4271
4384
  if (!(clauseTerm instanceof FormulaTerm)) return [];
@@ -4294,7 +4407,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4294
4407
  }
4295
4408
 
4296
4409
  // log:forAllIn (scoped)
4297
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'forAllIn') {
4410
+ if (pv === LOG_NS + 'forAllIn') {
4298
4411
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4299
4412
  const [whereClause, thenClause] = g.s.elems;
4300
4413
  if (!(whereClause instanceof FormulaTerm) || !(thenClause instanceof FormulaTerm)) return [];
@@ -4324,7 +4437,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4324
4437
  }
4325
4438
 
4326
4439
  // log:skolem
4327
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'skolem') {
4440
+ if (pv === LOG_NS + 'skolem') {
4328
4441
  // Subject must be ground; commonly a list, but we allow any ground term.
4329
4442
  if (!isGroundTerm(g.s)) return [];
4330
4443
 
@@ -4341,7 +4454,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4341
4454
  }
4342
4455
 
4343
4456
  // log:uri
4344
- if (g.p instanceof Iri && g.p.value === LOG_NS + 'uri') {
4457
+ if (pv === LOG_NS + 'uri') {
4345
4458
  // Direction 1: subject is an IRI -> object is its string representation
4346
4459
  if (g.s instanceof Iri) {
4347
4460
  const uriStr = g.s.value; // raw IRI string, e.g. "https://www.w3.org"
@@ -4370,7 +4483,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4370
4483
  // -----------------------------------------------------------------
4371
4484
 
4372
4485
  // string:concatenation
4373
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'concatenation') {
4486
+ if (pv === STRING_NS + 'concatenation') {
4374
4487
  if (!(g.s instanceof ListTerm)) return [];
4375
4488
  const parts = [];
4376
4489
  for (const t of g.s.elems) {
@@ -4390,7 +4503,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4390
4503
  }
4391
4504
 
4392
4505
  // string:contains
4393
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'contains') {
4506
+ if (pv === STRING_NS + 'contains') {
4394
4507
  const sStr = termToJsString(g.s);
4395
4508
  const oStr = termToJsString(g.o);
4396
4509
  if (sStr === null || oStr === null) return [];
@@ -4398,7 +4511,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4398
4511
  }
4399
4512
 
4400
4513
  // string:containsIgnoringCase
4401
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'containsIgnoringCase') {
4514
+ if (pv === STRING_NS + 'containsIgnoringCase') {
4402
4515
  const sStr = termToJsString(g.s);
4403
4516
  const oStr = termToJsString(g.o);
4404
4517
  if (sStr === null || oStr === null) return [];
@@ -4406,7 +4519,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4406
4519
  }
4407
4520
 
4408
4521
  // string:endsWith
4409
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'endsWith') {
4522
+ if (pv === STRING_NS + 'endsWith') {
4410
4523
  const sStr = termToJsString(g.s);
4411
4524
  const oStr = termToJsString(g.o);
4412
4525
  if (sStr === null || oStr === null) return [];
@@ -4414,7 +4527,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4414
4527
  }
4415
4528
 
4416
4529
  // string:equalIgnoringCase
4417
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'equalIgnoringCase') {
4530
+ if (pv === STRING_NS + 'equalIgnoringCase') {
4418
4531
  const sStr = termToJsString(g.s);
4419
4532
  const oStr = termToJsString(g.o);
4420
4533
  if (sStr === null || oStr === null) return [];
@@ -4423,7 +4536,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4423
4536
 
4424
4537
  // string:format
4425
4538
  // (limited: only %s and %% are supported, anything else ⇒ builtin fails)
4426
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'format') {
4539
+ if (pv === STRING_NS + 'format') {
4427
4540
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 1) return [];
4428
4541
  const fmtStr = termToJsString(g.s.elems[0]);
4429
4542
  if (fmtStr === null) return [];
@@ -4450,10 +4563,10 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4450
4563
 
4451
4564
  // string:jsonPointer
4452
4565
  // Schema: ( $jsonText $pointer ) string:jsonPointer $value
4453
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'jsonPointer') {
4566
+ if (pv === STRING_NS + 'jsonPointer') {
4454
4567
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4455
4568
 
4456
- const jsonText = termToJsonText(g.s.elems[0]); // <-- changed
4569
+ const jsonText = termToJsonText(g.s.elems[0]);
4457
4570
  const ptr = termToJsStringDecoded(g.s.elems[1]);
4458
4571
  if (jsonText === null || ptr === null) return [];
4459
4572
 
@@ -4465,7 +4578,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4465
4578
  }
4466
4579
 
4467
4580
  // string:greaterThan
4468
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'greaterThan') {
4581
+ if (pv === STRING_NS + 'greaterThan') {
4469
4582
  const sStr = termToJsString(g.s);
4470
4583
  const oStr = termToJsString(g.o);
4471
4584
  if (sStr === null || oStr === null) return [];
@@ -4473,7 +4586,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4473
4586
  }
4474
4587
 
4475
4588
  // string:lessThan
4476
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'lessThan') {
4589
+ if (pv === STRING_NS + 'lessThan') {
4477
4590
  const sStr = termToJsString(g.s);
4478
4591
  const oStr = termToJsString(g.o);
4479
4592
  if (sStr === null || oStr === null) return [];
@@ -4481,7 +4594,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4481
4594
  }
4482
4595
 
4483
4596
  // string:matches
4484
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'matches') {
4597
+ if (pv === STRING_NS + 'matches') {
4485
4598
  const sStr = termToJsString(g.s);
4486
4599
  const pattern = termToJsString(g.o);
4487
4600
  if (sStr === null || pattern === null) return [];
@@ -4496,7 +4609,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4496
4609
  }
4497
4610
 
4498
4611
  // string:notEqualIgnoringCase
4499
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notEqualIgnoringCase') {
4612
+ if (pv === STRING_NS + 'notEqualIgnoringCase') {
4500
4613
  const sStr = termToJsString(g.s);
4501
4614
  const oStr = termToJsString(g.o);
4502
4615
  if (sStr === null || oStr === null) return [];
@@ -4504,7 +4617,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4504
4617
  }
4505
4618
 
4506
4619
  // string:notGreaterThan (≤ in Unicode code order)
4507
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notGreaterThan') {
4620
+ if (pv === STRING_NS + 'notGreaterThan') {
4508
4621
  const sStr = termToJsString(g.s);
4509
4622
  const oStr = termToJsString(g.o);
4510
4623
  if (sStr === null || oStr === null) return [];
@@ -4512,7 +4625,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4512
4625
  }
4513
4626
 
4514
4627
  // string:notLessThan (≥ in Unicode code order)
4515
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notLessThan') {
4628
+ if (pv === STRING_NS + 'notLessThan') {
4516
4629
  const sStr = termToJsString(g.s);
4517
4630
  const oStr = termToJsString(g.o);
4518
4631
  if (sStr === null || oStr === null) return [];
@@ -4520,7 +4633,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4520
4633
  }
4521
4634
 
4522
4635
  // string:notMatches
4523
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'notMatches') {
4636
+ if (pv === STRING_NS + 'notMatches') {
4524
4637
  const sStr = termToJsString(g.s);
4525
4638
  const pattern = termToJsString(g.o);
4526
4639
  if (sStr === null || pattern === null) return [];
@@ -4534,7 +4647,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4534
4647
  }
4535
4648
 
4536
4649
  // string:replace
4537
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'replace') {
4650
+ if (pv === STRING_NS + 'replace') {
4538
4651
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 3) return [];
4539
4652
  const dataStr = termToJsString(g.s.elems[0]);
4540
4653
  const searchStr = termToJsString(g.s.elems[1]);
@@ -4562,7 +4675,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4562
4675
  }
4563
4676
 
4564
4677
  // string:scrape
4565
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'scrape') {
4678
+ if (pv === STRING_NS + 'scrape') {
4566
4679
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
4567
4680
  const dataStr = termToJsString(g.s.elems[0]);
4568
4681
  const pattern = termToJsString(g.s.elems[1]);
@@ -4591,7 +4704,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
4591
4704
  }
4592
4705
 
4593
4706
  // string:startsWith
4594
- if (g.p instanceof Iri && g.p.value === STRING_NS + 'startsWith') {
4707
+ if (pv === STRING_NS + 'startsWith') {
4595
4708
  const sStr = termToJsString(g.s);
4596
4709
  const oStr = termToJsString(g.o);
4597
4710
  if (sStr === null || oStr === null) return [];
@@ -4621,9 +4734,9 @@ function isBuiltinPred(p) {
4621
4734
  );
4622
4735
  }
4623
4736
 
4624
- // ============================================================================
4737
+ // ===========================================================================
4625
4738
  // Backward proof (SLD-style)
4626
- // ============================================================================
4739
+ // ===========================================================================
4627
4740
 
4628
4741
  function standardizeRule(rule, gen) {
4629
4742
  function renameTerm(t, vmap, genArr) {
@@ -4670,9 +4783,9 @@ function listHasTriple(list, tr) {
4670
4783
  return list.some((t) => triplesEqual(t, tr));
4671
4784
  }
4672
4785
 
4673
- // ============================================================================
4786
+ // ===========================================================================
4674
4787
  // Substitution compaction (to avoid O(depth^2) in deep backward chains)
4675
- // ============================================================================
4788
+ // ===========================================================================
4676
4789
  //
4677
4790
  // Why: backward chaining with standardizeRule introduces fresh variables at
4678
4791
  // each step. composeSubst frequently copies a growing substitution object.
@@ -4907,9 +5020,9 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen) {
4907
5020
  return results;
4908
5021
  }
4909
5022
 
4910
- // ============================================================================
5023
+ // ===========================================================================
4911
5024
  // Forward chaining to fixpoint
4912
- // ============================================================================
5025
+ // ===========================================================================
4913
5026
 
4914
5027
  function forwardChain(facts, forwardRules, backRules) {
4915
5028
  ensureFactIndexes(facts);
@@ -5093,9 +5206,9 @@ function forwardChain(facts, forwardRules, backRules) {
5093
5206
  return derivedForward;
5094
5207
  }
5095
5208
 
5096
- // ============================================================================
5209
+ // ===========================================================================
5097
5210
  // Pretty printing as N3/Turtle
5098
- // ============================================================================
5211
+ // ===========================================================================
5099
5212
 
5100
5213
  function termToN3(t, pref) {
5101
5214
  if (t instanceof Iri) {
@@ -5262,179 +5375,9 @@ function printExplanation(df, prefixes) {
5262
5375
  console.log('# ----------------------------------------------------------------------\n');
5263
5376
  }
5264
5377
 
5265
- // ============================================================================
5266
- // Misc helpers
5267
- // ============================================================================
5268
-
5269
- // Turn RDF Collections described with rdf:first/rdf:rest (+ rdf:nil) into ListTerm terms.
5270
- // This mutates triples/rules in-place so list:* builtins work on RDF-serialized lists too.
5271
- function materializeRdfLists(triples, forwardRules, backwardRules) {
5272
- const RDF_FIRST = RDF_NS + 'first';
5273
- const RDF_REST = RDF_NS + 'rest';
5274
- const RDF_NIL = RDF_NS + 'nil';
5275
-
5276
- function nodeKey(t) {
5277
- if (t instanceof Blank) return 'B:' + t.label;
5278
- if (t instanceof Iri) return 'I:' + t.value;
5279
- return null;
5280
- }
5281
-
5282
- // Collect first/rest arcs from *input triples*
5283
- const firstMap = new Map(); // key(subject) -> Term (object)
5284
- const restMap = new Map(); // key(subject) -> Term (object)
5285
- for (const tr of triples) {
5286
- if (!(tr.p instanceof Iri)) continue;
5287
- const k = nodeKey(tr.s);
5288
- if (!k) continue;
5289
- if (tr.p.value === RDF_FIRST) firstMap.set(k, tr.o);
5290
- else if (tr.p.value === RDF_REST) restMap.set(k, tr.o);
5291
- }
5292
- if (!firstMap.size && !restMap.size) return;
5293
-
5294
- const cache = new Map(); // key(node) -> ListTerm
5295
- const visiting = new Set(); // cycle guard
5296
-
5297
- function buildListForKey(k) {
5298
- if (cache.has(k)) return cache.get(k);
5299
- if (visiting.has(k)) return null; // cycle => not a well-formed list
5300
- visiting.add(k);
5301
-
5302
- // rdf:nil => ()
5303
- if (k === 'I:' + RDF_NIL) {
5304
- const empty = new ListTerm([]);
5305
- cache.set(k, empty);
5306
- visiting.delete(k);
5307
- return empty;
5308
- }
5309
-
5310
- const head = firstMap.get(k);
5311
- const tail = restMap.get(k);
5312
- if (head === undefined || tail === undefined) {
5313
- visiting.delete(k);
5314
- return null; // not a full cons cell
5315
- }
5316
-
5317
- const headTerm = rewriteTerm(head);
5318
-
5319
- let tailListElems = null;
5320
- if (tail instanceof Iri && tail.value === RDF_NIL) {
5321
- tailListElems = [];
5322
- } else {
5323
- const tk = nodeKey(tail);
5324
- if (!tk) {
5325
- visiting.delete(k);
5326
- return null;
5327
- }
5328
- const tailList = buildListForKey(tk);
5329
- if (!tailList) {
5330
- visiting.delete(k);
5331
- return null;
5332
- }
5333
- tailListElems = tailList.elems;
5334
- }
5335
-
5336
- const out = new ListTerm([headTerm, ...tailListElems]);
5337
- cache.set(k, out);
5338
- visiting.delete(k);
5339
- return out;
5340
- }
5341
-
5342
- function rewriteTerm(t) {
5343
- // Replace list nodes (Blank/Iri) by their constructed ListTerm when possible
5344
- const k = nodeKey(t);
5345
- if (k) {
5346
- const built = buildListForKey(k);
5347
- if (built) return built;
5348
- // Also rewrite rdf:nil even if not otherwise referenced
5349
- if (t instanceof Iri && t.value === RDF_NIL) return new ListTerm([]);
5350
- return t;
5351
- }
5352
- if (t instanceof ListTerm) {
5353
- let changed = false;
5354
- const elems = t.elems.map((e) => {
5355
- const r = rewriteTerm(e);
5356
- if (r !== e) changed = true;
5357
- return r;
5358
- });
5359
- return changed ? new ListTerm(elems) : t;
5360
- }
5361
- if (t instanceof OpenListTerm) {
5362
- let changed = false;
5363
- const prefix = t.prefix.map((e) => {
5364
- const r = rewriteTerm(e);
5365
- if (r !== e) changed = true;
5366
- return r;
5367
- });
5368
- return changed ? new OpenListTerm(prefix, t.tailVar) : t;
5369
- }
5370
- if (t instanceof FormulaTerm) {
5371
- for (const tr of t.triples) rewriteTriple(tr);
5372
- return t;
5373
- }
5374
- return t;
5375
- }
5376
-
5377
- function rewriteTriple(tr) {
5378
- tr.s = rewriteTerm(tr.s);
5379
- tr.p = rewriteTerm(tr.p);
5380
- tr.o = rewriteTerm(tr.o);
5381
- }
5382
-
5383
- // Pre-build all reachable list heads
5384
- for (const k of firstMap.keys()) buildListForKey(k);
5385
-
5386
- // Rewrite input triples + rules in-place
5387
- for (const tr of triples) rewriteTriple(tr);
5388
- for (const r of forwardRules) {
5389
- for (const tr of r.premise) rewriteTriple(tr);
5390
- for (const tr of r.conclusion) rewriteTriple(tr);
5391
- }
5392
- for (const r of backwardRules) {
5393
- for (const tr of r.premise) rewriteTriple(tr);
5394
- for (const tr of r.conclusion) rewriteTriple(tr);
5395
- }
5396
- }
5397
-
5398
- function localIsoDateTimeString(d) {
5399
- function pad(n, width = 2) {
5400
- return String(n).padStart(width, '0');
5401
- }
5402
- const year = d.getFullYear();
5403
- const month = d.getMonth() + 1;
5404
- const day = d.getDate();
5405
- const hour = d.getHours();
5406
- const min = d.getMinutes();
5407
- const sec = d.getSeconds();
5408
- const ms = d.getMilliseconds();
5409
- const offsetMin = -d.getTimezoneOffset(); // minutes east of UTC
5410
- const sign = offsetMin >= 0 ? '+' : '-';
5411
- const abs = Math.abs(offsetMin);
5412
- const oh = Math.floor(abs / 60);
5413
- const om = abs % 60;
5414
- const msPart = ms ? '.' + String(ms).padStart(3, '0') : '';
5415
- return (
5416
- pad(year, 4) +
5417
- '-' +
5418
- pad(month) +
5419
- '-' +
5420
- pad(day) +
5421
- 'T' +
5422
- pad(hour) +
5423
- ':' +
5424
- pad(min) +
5425
- ':' +
5426
- pad(sec) +
5427
- msPart +
5428
- sign +
5429
- pad(oh) +
5430
- ':' +
5431
- pad(om)
5432
- );
5433
- }
5434
-
5435
- // ============================================================================
5378
+ // ===========================================================================
5436
5379
  // CLI entry point
5437
- // ============================================================================
5380
+ // ===========================================================================
5438
5381
  function main() {
5439
5382
  // Drop "node" and script name; keep only user-provided args
5440
5383
  const argv = process.argv.slice(2);