eyeling 1.6.3 → 1.6.4

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 +319 -228
  2. package/package.json +1 -1
package/eyeling.js CHANGED
@@ -1263,24 +1263,50 @@ function termsEqual(a, b) {
1263
1263
  if (a === b) return true;
1264
1264
  if (!a || !b) return false;
1265
1265
  if (a.constructor !== b.constructor) return false;
1266
+
1266
1267
  if (a instanceof Iri) return a.value === b.value;
1268
+
1267
1269
  if (a instanceof Literal) {
1268
1270
  if (a.value === b.value) return true;
1271
+
1272
+ // Plain "abc" == "abc"^^xsd:string (but not language-tagged strings)
1269
1273
  if (literalsEquivalentAsXsdString(a.value, b.value)) return true;
1270
1274
 
1271
- // Keep in sync with unifyTerm(): numeric-value equality (EYE-style)
1272
- const av = parseNumberLiteral(a);
1273
- const bv = parseNumberLiteral(b);
1274
- if (av !== null && bv !== null) {
1275
- if (typeof av === 'bigint' && typeof bv === 'bigint') return av === bv;
1276
- const an = typeof av === 'bigint' ? Number(av) : av;
1277
- const bn = typeof bv === 'bigint' ? Number(bv) : bv;
1278
- return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
1275
+ // Keep in sync with unifyTerm(): numeric-value equality, datatype-aware.
1276
+ const ai = parseNumericLiteralInfo(a);
1277
+ const bi = parseNumericLiteralInfo(b);
1278
+
1279
+ if (ai && bi) {
1280
+ // Same datatype => compare values
1281
+ if (ai.dt === bi.dt) {
1282
+ if (ai.kind === 'bigint' && bi.kind === 'bigint') return ai.value === bi.value;
1283
+
1284
+ const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
1285
+ const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
1286
+ return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
1287
+ }
1288
+
1289
+ // integer <-> decimal: allow exact equality (but NOT with double)
1290
+ const intDt = XSD_NS + 'integer';
1291
+ const decDt = XSD_NS + 'decimal';
1292
+ if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
1293
+ const intInfo = ai.dt === intDt ? ai : bi;
1294
+ const decInfo = ai.dt === decDt ? ai : bi;
1295
+
1296
+ const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
1297
+ if (dec) {
1298
+ const scaledInt = intInfo.value * pow10n(dec.scale);
1299
+ return scaledInt === dec.num;
1300
+ }
1301
+ }
1279
1302
  }
1303
+
1280
1304
  return false;
1281
1305
  }
1306
+
1282
1307
  if (a instanceof Var) return a.name === b.name;
1283
1308
  if (a instanceof Blank) return a.label === b.label;
1309
+
1284
1310
  if (a instanceof ListTerm) {
1285
1311
  if (a.elems.length !== b.elems.length) return false;
1286
1312
  for (let i = 0; i < a.elems.length; i++) {
@@ -1288,6 +1314,7 @@ function termsEqual(a, b) {
1288
1314
  }
1289
1315
  return true;
1290
1316
  }
1317
+
1291
1318
  if (a instanceof OpenListTerm) {
1292
1319
  if (a.tailVar !== b.tailVar) return false;
1293
1320
  if (a.prefix.length !== b.prefix.length) return false;
@@ -1296,9 +1323,79 @@ function termsEqual(a, b) {
1296
1323
  }
1297
1324
  return true;
1298
1325
  }
1326
+
1299
1327
  if (a instanceof FormulaTerm) {
1300
1328
  return alphaEqFormulaTriples(a.triples, b.triples);
1301
1329
  }
1330
+
1331
+ return false;
1332
+ }
1333
+
1334
+ function termsEqualNoIntDecimal(a, b) {
1335
+ if (a === b) return true;
1336
+ if (!a || !b) return false;
1337
+ if (a.constructor !== b.constructor) return false;
1338
+
1339
+ if (a instanceof Iri) return a.value === b.value;
1340
+
1341
+ if (a instanceof Literal) {
1342
+ if (a.value === b.value) return true;
1343
+
1344
+ // Plain "abc" == "abc"^^xsd:string (but not language-tagged)
1345
+ if (literalsEquivalentAsXsdString(a.value, b.value)) return true;
1346
+
1347
+ // Numeric equality ONLY when datatypes agree (no integer<->decimal here)
1348
+ const ai = parseNumericLiteralInfo(a);
1349
+ const bi = parseNumericLiteralInfo(b);
1350
+ if (ai && bi && ai.dt === bi.dt) {
1351
+ // integer: exact bigint
1352
+ if (ai.kind === 'bigint' && bi.kind === 'bigint') return ai.value === bi.value;
1353
+
1354
+ // decimal: compare exactly via num/scale if possible
1355
+ if (ai.dt === XSD_NS + 'decimal') {
1356
+ const da = parseXsdDecimalToBigIntScale(ai.lexStr);
1357
+ const db = parseXsdDecimalToBigIntScale(bi.lexStr);
1358
+ if (da && db) {
1359
+ const scale = Math.max(da.scale, db.scale);
1360
+ const na = da.num * pow10n(scale - da.scale);
1361
+ const nb = db.num * pow10n(scale - db.scale);
1362
+ return na === nb;
1363
+ }
1364
+ }
1365
+
1366
+ // double/float-ish: JS number (same as your normal same-dt path)
1367
+ const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
1368
+ const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
1369
+ return !Number.isNaN(an) && !Number.isNaN(bn) && an === bn;
1370
+ }
1371
+
1372
+ return false;
1373
+ }
1374
+
1375
+ if (a instanceof Var) return a.name === b.name;
1376
+ if (a instanceof Blank) return a.label === b.label;
1377
+
1378
+ if (a instanceof ListTerm) {
1379
+ if (a.elems.length !== b.elems.length) return false;
1380
+ for (let i = 0; i < a.elems.length; i++) {
1381
+ if (!termsEqualNoIntDecimal(a.elems[i], b.elems[i])) return false;
1382
+ }
1383
+ return true;
1384
+ }
1385
+
1386
+ if (a instanceof OpenListTerm) {
1387
+ if (a.tailVar !== b.tailVar) return false;
1388
+ if (a.prefix.length !== b.prefix.length) return false;
1389
+ for (let i = 0; i < a.prefix.length; i++) {
1390
+ if (!termsEqualNoIntDecimal(a.prefix[i], b.prefix[i])) return false;
1391
+ }
1392
+ return true;
1393
+ }
1394
+
1395
+ if (a instanceof FormulaTerm) {
1396
+ return alphaEqFormulaTriples(a.triples, b.triples);
1397
+ }
1398
+
1302
1399
  return false;
1303
1400
  }
1304
1401
 
@@ -1906,13 +2003,30 @@ function unifyTerm(a, b, subst) {
1906
2003
  const ai = parseNumericLiteralInfo(a);
1907
2004
  const bi = parseNumericLiteralInfo(b);
1908
2005
 
1909
- if (ai && bi && ai.dt === bi.dt) {
1910
- if (ai.kind === 'bigint' && bi.kind === 'bigint') {
1911
- if (ai.value === bi.value) return { ...subst };
1912
- } else {
1913
- const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
1914
- const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
1915
- if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
2006
+ if (ai && bi) {
2007
+ // same datatype: keep existing behavior
2008
+ if (ai.dt === bi.dt) {
2009
+ if (ai.kind === 'bigint' && bi.kind === 'bigint') {
2010
+ if (ai.value === bi.value) return { ...subst };
2011
+ } else {
2012
+ const an = ai.kind === 'bigint' ? Number(ai.value) : ai.value;
2013
+ const bn = bi.kind === 'bigint' ? Number(bi.value) : bi.value;
2014
+ if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
2015
+ }
2016
+ }
2017
+
2018
+ // integer <-> decimal: allow exact equality (but NOT with double)
2019
+ const intDt = XSD_NS + 'integer';
2020
+ const decDt = XSD_NS + 'decimal';
2021
+ if ((ai.dt === intDt && bi.dt === decDt) || (ai.dt === decDt && bi.dt === intDt)) {
2022
+ const intInfo = ai.dt === intDt ? ai : bi;
2023
+ const decInfo = ai.dt === decDt ? ai : bi;
2024
+
2025
+ const dec = parseXsdDecimalToBigIntScale(decInfo.lexStr);
2026
+ if (dec) {
2027
+ const scaledInt = intInfo.value * pow10n(dec.scale);
2028
+ if (scaledInt === dec.num) return { ...subst };
2029
+ }
1916
2030
  }
1917
2031
  }
1918
2032
  }
@@ -2388,6 +2502,44 @@ function formatNum(n) {
2388
2502
  return String(n);
2389
2503
  }
2390
2504
 
2505
+ function parseXsdDecimalToBigIntScale(s) {
2506
+ let t = String(s).trim();
2507
+ let sign = 1n;
2508
+
2509
+ if (t.startsWith('+')) t = t.slice(1);
2510
+ else if (t.startsWith('-')) {
2511
+ sign = -1n;
2512
+ t = t.slice(1);
2513
+ }
2514
+
2515
+ // xsd:decimal lexical: (\d+(\.\d*)?|\.\d+)
2516
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(t)) return null;
2517
+
2518
+ let intPart = '0';
2519
+ let fracPart = '';
2520
+
2521
+ if (t.includes('.')) {
2522
+ const parts = t.split('.');
2523
+ intPart = parts[0] === '' ? '0' : parts[0];
2524
+ fracPart = parts[1] || '';
2525
+ } else {
2526
+ intPart = t;
2527
+ }
2528
+
2529
+ // normalize
2530
+ intPart = intPart.replace(/^0+(?=\d)/, '');
2531
+ fracPart = fracPart.replace(/0+$/, ''); // drop trailing zeros
2532
+
2533
+ const scale = fracPart.length;
2534
+ const digits = intPart + fracPart || '0';
2535
+
2536
+ return { num: sign * BigInt(digits), scale };
2537
+ }
2538
+
2539
+ function pow10n(k) {
2540
+ return 10n ** BigInt(k);
2541
+ }
2542
+
2391
2543
  function parseXsdDateTerm(t) {
2392
2544
  if (!(t instanceof Literal)) return null;
2393
2545
  const [lex, dt] = literalParts(t.value);
@@ -2572,28 +2724,23 @@ function parseNumericLiteralInfo(t) {
2572
2724
  let lexStr;
2573
2725
 
2574
2726
  if (dt2 !== null) {
2575
- // Only handle the numeric datatypes we want to unify by value
2576
- if (
2577
- dt2 !== XSD_NS + 'integer' &&
2578
- dt2 !== XSD_NS + 'decimal' &&
2579
- dt2 !== XSD_NS + 'double'
2580
- ) return null;
2581
-
2582
- // typed numerics are like "1.1"^^<...>
2727
+ if (dt2 !== XSD_NS + 'integer' && dt2 !== XSD_NS + 'decimal' && dt2 !== XSD_NS + 'double') return null;
2583
2728
  lexStr = stripQuotes(lex);
2584
2729
  } else {
2585
- // Raw numeric token like 42, 1.1, 1.1e0 (NOT quoted strings, NOT lang-tagged)
2586
2730
  if (typeof v !== 'string') return null;
2587
- if (v.startsWith('"')) return null;
2731
+ if (v.startsWith('"')) return null; // exclude quoted strings
2588
2732
  if (!/^[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?$/.test(v)) return null;
2589
2733
 
2590
- dt2 = numericDatatypeFromLex(v);
2734
+ if (/[eE]/.test(v)) dt2 = XSD_NS + 'double';
2735
+ else if (v.includes('.')) dt2 = XSD_NS + 'decimal';
2736
+ else dt2 = XSD_NS + 'integer';
2737
+
2591
2738
  lexStr = v;
2592
2739
  }
2593
2740
 
2594
2741
  if (dt2 === XSD_NS + 'integer') {
2595
2742
  try {
2596
- return { dt: dt2, kind: 'bigint', value: BigInt(lexStr) };
2743
+ return { dt: dt2, kind: 'bigint', value: BigInt(lexStr), lexStr };
2597
2744
  } catch {
2598
2745
  return null;
2599
2746
  }
@@ -2601,7 +2748,48 @@ function parseNumericLiteralInfo(t) {
2601
2748
 
2602
2749
  const num = Number(lexStr);
2603
2750
  if (Number.isNaN(num)) return null;
2604
- return { dt: dt2, kind: 'number', value: num };
2751
+ return { dt: dt2, kind: 'number', value: num, lexStr };
2752
+ }
2753
+
2754
+ function evalUnaryMathRel(g, subst, forwardFn, inverseFn /* may be null */) {
2755
+ const sIsUnbound = g.s instanceof Var || g.s instanceof Blank;
2756
+ const oIsUnbound = g.o instanceof Var || g.o instanceof Blank;
2757
+
2758
+ const a = parseNum(g.s); // subject numeric?
2759
+ const b = parseNum(g.o); // object numeric?
2760
+
2761
+ // Forward: s numeric => compute o
2762
+ if (a !== null) {
2763
+ const outVal = forwardFn(a);
2764
+ if (!Number.isFinite(outVal)) return [];
2765
+ if (g.o instanceof Var) {
2766
+ const s2 = { ...subst };
2767
+ s2[g.o.name] = new Literal(formatNum(outVal));
2768
+ return [s2];
2769
+ }
2770
+ if (g.o instanceof Blank) return [{ ...subst }];
2771
+ if (numEqualTerm(g.o, outVal)) return [{ ...subst }];
2772
+ return [];
2773
+ }
2774
+
2775
+ // Reverse (bounded): o numeric => compute s
2776
+ if (b !== null && typeof inverseFn === 'function') {
2777
+ const inVal = inverseFn(b);
2778
+ if (!Number.isFinite(inVal)) return [];
2779
+ if (g.s instanceof Var) {
2780
+ const s2 = { ...subst };
2781
+ s2[g.s.name] = new Literal(formatNum(inVal));
2782
+ return [s2];
2783
+ }
2784
+ if (g.s instanceof Blank) return [{ ...subst }];
2785
+ if (numEqualTerm(g.s, inVal)) return [{ ...subst }];
2786
+ return [];
2787
+ }
2788
+
2789
+ // Fully unbound: treat as satisfiable (avoid infinite enumeration)
2790
+ if (sIsUnbound && oIsUnbound) return [{ ...subst }];
2791
+
2792
+ return [];
2605
2793
  }
2606
2794
 
2607
2795
  // ============================================================================
@@ -2993,28 +3181,6 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
2993
3181
  }
2994
3182
  }
2995
3183
 
2996
- // math:negation
2997
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'negation') {
2998
- const a = parseNum(g.s);
2999
- if (a !== null && g.o instanceof Var) {
3000
- const s2 = { ...subst };
3001
- s2[g.o.name] = new Literal(formatNum(-a));
3002
- return [s2];
3003
- }
3004
- const b = parseNum(g.o);
3005
- if (g.s instanceof Var && b !== null) {
3006
- const s2 = { ...subst };
3007
- s2[g.s.name] = new Literal(formatNum(-b));
3008
- return [s2];
3009
- }
3010
- const a2 = parseNum(g.s);
3011
- const b2 = parseNum(g.o);
3012
- if (a2 !== null && b2 !== null) {
3013
- if (Math.abs(-a2 - b2) < 1e-9) return [{ ...subst }];
3014
- }
3015
- return [];
3016
- }
3017
-
3018
3184
  // math:absoluteValue
3019
3185
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'absoluteValue') {
3020
3186
  const a = parseNum(g.s);
@@ -3030,135 +3196,61 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3030
3196
  return [];
3031
3197
  }
3032
3198
 
3033
- // math:cos
3034
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'cos') {
3035
- const a = parseNum(g.s);
3036
- if (a !== null) {
3037
- const cVal = Math.cos(a);
3038
- if (g.o instanceof Var) {
3039
- const s2 = { ...subst };
3040
- s2[g.o.name] = new Literal(formatNum(cVal));
3041
- return [s2];
3042
- }
3043
- if (numEqualTerm(g.o, cVal)) {
3044
- return [{ ...subst }];
3045
- }
3046
- }
3047
- return [];
3048
- }
3049
-
3050
- // math:sin
3051
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sin') {
3052
- const a = parseNum(g.s);
3053
- if (a !== null) {
3054
- const cVal = Math.sin(a);
3055
- if (g.o instanceof Var) {
3056
- const s2 = { ...subst };
3057
- s2[g.o.name] = new Literal(formatNum(cVal));
3058
- return [s2];
3059
- }
3060
- if (numEqualTerm(g.o, cVal)) {
3061
- return [{ ...subst }];
3062
- }
3063
- }
3064
- return [];
3065
- }
3066
-
3067
3199
  // math:acos
3068
3200
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'acos') {
3069
- const a = parseNum(g.s);
3070
- if (a !== null) {
3071
- const cVal = Math.acos(a);
3072
- if (Number.isFinite(cVal)) {
3073
- if (g.o instanceof Var) {
3074
- const s2 = { ...subst };
3075
- s2[g.o.name] = new Literal(formatNum(cVal));
3076
- return [s2];
3077
- }
3078
- if (numEqualTerm(g.o, cVal)) {
3079
- return [{ ...subst }];
3080
- }
3081
- }
3082
- }
3083
- return [];
3201
+ return evalUnaryMathRel(g, subst, Math.acos, Math.cos);
3084
3202
  }
3085
3203
 
3086
3204
  // math:asin
3087
3205
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'asin') {
3088
- const a = parseNum(g.s);
3089
- if (a !== null) {
3090
- const cVal = Math.asin(a);
3091
- if (Number.isFinite(cVal)) {
3092
- if (g.o instanceof Var) {
3093
- const s2 = { ...subst };
3094
- s2[g.o.name] = new Literal(formatNum(cVal));
3095
- return [s2];
3096
- }
3097
- if (numEqualTerm(g.o, cVal)) {
3098
- return [{ ...subst }];
3099
- }
3100
- }
3101
- }
3102
- return [];
3206
+ return evalUnaryMathRel(g, subst, Math.asin, Math.sin);
3103
3207
  }
3104
3208
 
3105
3209
  // math:atan
3106
3210
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'atan') {
3107
- const a = parseNum(g.s);
3108
- if (a !== null) {
3109
- const cVal = Math.atan(a);
3110
- if (Number.isFinite(cVal)) {
3111
- if (g.o instanceof Var) {
3112
- const s2 = { ...subst };
3113
- s2[g.o.name] = new Literal(formatNum(cVal));
3114
- return [s2];
3115
- }
3116
- if (numEqualTerm(g.o, cVal)) {
3117
- return [{ ...subst }];
3118
- }
3119
- }
3120
- }
3121
- return [];
3211
+ return evalUnaryMathRel(g, subst, Math.atan, Math.tan);
3122
3212
  }
3123
3213
 
3124
- // math:cosh
3125
- // Hyperbolic cosine
3214
+ // math:sin (inverse uses principal asin)
3215
+ if (g.p instanceof Iri && g.p.value === MATH_NS + 'sin') {
3216
+ return evalUnaryMathRel(g, subst, Math.sin, Math.asin);
3217
+ }
3218
+
3219
+ // math:cos (inverse uses principal acos)
3220
+ if (g.p instanceof Iri && g.p.value === MATH_NS + 'cos') {
3221
+ return evalUnaryMathRel(g, subst, Math.cos, Math.acos);
3222
+ }
3223
+
3224
+ // math:tan (inverse uses principal atan)
3225
+ if (g.p instanceof Iri && g.p.value === MATH_NS + 'tan') {
3226
+ return evalUnaryMathRel(g, subst, Math.tan, Math.atan);
3227
+ }
3228
+
3229
+ // math:sinh / cosh / tanh (guard for JS availability)
3230
+ if (g.p instanceof Iri && g.p.value === MATH_NS + 'sinh') {
3231
+ if (typeof Math.sinh !== 'function' || typeof Math.asinh !== 'function') return [];
3232
+ return evalUnaryMathRel(g, subst, Math.sinh, Math.asinh);
3233
+ }
3126
3234
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'cosh') {
3127
- const a = parseNum(g.s);
3128
- if (a !== null && typeof Math.cosh === 'function') {
3129
- const cVal = Math.cosh(a);
3130
- if (Number.isFinite(cVal)) {
3131
- if (g.o instanceof Var) {
3132
- const s2 = { ...subst };
3133
- s2[g.o.name] = new Literal(formatNum(cVal));
3134
- return [s2];
3135
- }
3136
- if (numEqualTerm(g.o, cVal)) {
3137
- return [{ ...subst }];
3138
- }
3139
- }
3140
- }
3141
- return [];
3235
+ if (typeof Math.cosh !== 'function' || typeof Math.acosh !== 'function') return [];
3236
+ return evalUnaryMathRel(g, subst, Math.cosh, Math.acosh);
3237
+ }
3238
+ if (g.p instanceof Iri && g.p.value === MATH_NS + 'tanh') {
3239
+ if (typeof Math.tanh !== 'function' || typeof Math.atanh !== 'function') return [];
3240
+ return evalUnaryMathRel(g, subst, Math.tanh, Math.atanh);
3142
3241
  }
3143
3242
 
3144
- // math:degrees
3145
- // Convert radians -> degrees
3243
+ // math:degrees (inverse is radians)
3146
3244
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'degrees') {
3147
- const a = parseNum(g.s);
3148
- if (a !== null) {
3149
- const cVal = (a * 180.0) / Math.PI;
3150
- if (Number.isFinite(cVal)) {
3151
- if (g.o instanceof Var) {
3152
- const s2 = { ...subst };
3153
- s2[g.o.name] = new Literal(formatNum(cVal));
3154
- return [s2];
3155
- }
3156
- if (numEqualTerm(g.o, cVal)) {
3157
- return [{ ...subst }];
3158
- }
3159
- }
3160
- }
3161
- return [];
3245
+ const toDeg = (rad) => (rad * 180.0) / Math.PI;
3246
+ const toRad = (deg) => (deg * Math.PI) / 180.0;
3247
+ return evalUnaryMathRel(g, subst, toDeg, toRad);
3248
+ }
3249
+
3250
+ // math:negation (inverse is itself)
3251
+ if (g.p instanceof Iri && g.p.value === MATH_NS + 'negation') {
3252
+ const neg = (x) => -x;
3253
+ return evalUnaryMathRel(g, subst, neg, neg);
3162
3254
  }
3163
3255
 
3164
3256
  // math:remainder
@@ -3201,63 +3293,6 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3201
3293
  return s2 !== null ? [s2] : [];
3202
3294
  }
3203
3295
 
3204
- // math:sinh
3205
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sinh') {
3206
- const a = parseNum(g.s);
3207
- if (a !== null && typeof Math.sinh === 'function') {
3208
- const cVal = Math.sinh(a);
3209
- if (Number.isFinite(cVal)) {
3210
- if (g.o instanceof Var) {
3211
- const s2 = { ...subst };
3212
- s2[g.o.name] = new Literal(formatNum(cVal));
3213
- return [s2];
3214
- }
3215
- if (numEqualTerm(g.o, cVal)) {
3216
- return [{ ...subst }];
3217
- }
3218
- }
3219
- }
3220
- return [];
3221
- }
3222
-
3223
- // math:tan
3224
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tan') {
3225
- const a = parseNum(g.s);
3226
- if (a !== null) {
3227
- const cVal = Math.tan(a);
3228
- if (Number.isFinite(cVal)) {
3229
- if (g.o instanceof Var) {
3230
- const s2 = { ...subst };
3231
- s2[g.o.name] = new Literal(formatNum(cVal));
3232
- return [s2];
3233
- }
3234
- if (numEqualTerm(g.o, cVal)) {
3235
- return [{ ...subst }];
3236
- }
3237
- }
3238
- }
3239
- return [];
3240
- }
3241
-
3242
- // math:tanh
3243
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tanh') {
3244
- const a = parseNum(g.s);
3245
- if (a !== null && typeof Math.tanh === 'function') {
3246
- const cVal = Math.tanh(a);
3247
- if (Number.isFinite(cVal)) {
3248
- if (g.o instanceof Var) {
3249
- const s2 = { ...subst };
3250
- s2[g.o.name] = new Literal(formatNum(cVal));
3251
- return [s2];
3252
- }
3253
- if (numEqualTerm(g.o, cVal)) {
3254
- return [{ ...subst }];
3255
- }
3256
- }
3257
- }
3258
- return [];
3259
- }
3260
-
3261
3296
  // -----------------------------------------------------------------
3262
3297
  // 4.3 time: builtins
3263
3298
  // -----------------------------------------------------------------
@@ -3390,12 +3425,37 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3390
3425
  if (!(g.s instanceof ListTerm)) return [];
3391
3426
  const xs = g.s.elems;
3392
3427
  const outs = [];
3428
+
3393
3429
  for (let i = 0; i < xs.length; i++) {
3394
3430
  const idxLit = new Literal(String(i)); // 0-based
3395
- const pair = new ListTerm([idxLit, xs[i]]);
3431
+ const val = xs[i];
3432
+
3433
+ // Fast path: object is exactly a 2-element list (idx, value)
3434
+ if (g.o instanceof ListTerm && g.o.elems.length === 2) {
3435
+ const [idxPat, valPat] = g.o.elems;
3436
+
3437
+ const s1 = unifyTerm(idxPat, idxLit, subst);
3438
+ if (s1 === null) continue;
3439
+
3440
+ // If value-pattern is ground after subst: require STRICT term equality
3441
+ const valPat2 = applySubstTerm(valPat, s1);
3442
+ if (isGroundTerm(valPat2)) {
3443
+ if (termsEqualNoIntDecimal(valPat2, val)) outs.push({ ...s1 });
3444
+ continue;
3445
+ }
3446
+
3447
+ // Otherwise, allow normal unification/binding
3448
+ const s2 = unifyTerm(valPat, val, s1);
3449
+ if (s2 !== null) outs.push(s2);
3450
+ continue;
3451
+ }
3452
+
3453
+ // Fallback: original behavior
3454
+ const pair = new ListTerm([idxLit, val]);
3396
3455
  const s2 = unifyTerm(g.o, pair, subst);
3397
3456
  if (s2 !== null) outs.push(s2);
3398
3457
  }
3458
+
3399
3459
  return outs;
3400
3460
  }
3401
3461
 
@@ -3418,17 +3478,35 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3418
3478
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3419
3479
  const [listTerm, indexTerm] = g.s.elems;
3420
3480
  if (!(listTerm instanceof ListTerm)) return [];
3481
+
3421
3482
  const xs = listTerm.elems;
3422
3483
  const outs = [];
3423
3484
 
3424
3485
  for (let i = 0; i < xs.length; i++) {
3425
3486
  const idxLit = new Literal(String(i)); // index starts at 0
3426
- let s1 = unifyTerm(indexTerm, idxLit, subst);
3427
- if (s1 === null) continue;
3428
- let s2 = unifyTerm(g.o, xs[i], s1);
3429
- if (s2 === null) continue;
3430
- outs.push(s2);
3487
+
3488
+ // --- index side: strict if ground, otherwise unify/bind
3489
+ let s1 = null;
3490
+ const idxPat2 = applySubstTerm(indexTerm, subst);
3491
+ if (isGroundTerm(idxPat2)) {
3492
+ if (!termsEqualNoIntDecimal(idxPat2, idxLit)) continue;
3493
+ s1 = { ...subst };
3494
+ } else {
3495
+ s1 = unifyTerm(indexTerm, idxLit, subst);
3496
+ if (s1 === null) continue;
3497
+ }
3498
+
3499
+ // --- value side: strict if ground, otherwise unify/bind
3500
+ const o2 = applySubstTerm(g.o, s1);
3501
+ if (isGroundTerm(o2)) {
3502
+ if (termsEqualNoIntDecimal(o2, xs[i])) outs.push({ ...s1 });
3503
+ continue;
3504
+ }
3505
+
3506
+ const s2 = unifyTerm(g.o, xs[i], s1);
3507
+ if (s2 !== null) outs.push(s2);
3431
3508
  }
3509
+
3432
3510
  return outs;
3433
3511
  }
3434
3512
 
@@ -3439,11 +3517,18 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3439
3517
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3440
3518
  const [listTerm, itemTerm] = g.s.elems;
3441
3519
  if (!(listTerm instanceof ListTerm)) return [];
3520
+
3521
+ // item must be bound
3522
+ const item2 = applySubstTerm(itemTerm, subst);
3523
+ if (!isGroundTerm(item2)) return [];
3524
+
3442
3525
  const xs = listTerm.elems;
3443
3526
  const filtered = [];
3444
3527
  for (const e of xs) {
3445
- if (!termsEqual(e, itemTerm)) filtered.push(e);
3528
+ // strict term match (still allows plain "abc" == "abc"^^xsd:string)
3529
+ if (!termsEqualNoIntDecimal(e, item2)) filtered.push(e);
3446
3530
  }
3531
+
3447
3532
  const resList = new ListTerm(filtered);
3448
3533
  const s2 = unifyTerm(g.o, resList, subst);
3449
3534
  return s2 !== null ? [s2] : [];
@@ -3471,10 +3556,16 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3471
3556
  return outs;
3472
3557
  }
3473
3558
 
3474
- // list:length
3559
+ // list:length (strict: do not accept integer<->decimal matches for a ground object)
3475
3560
  if (g.p instanceof Iri && g.p.value === LIST_NS + 'length') {
3476
3561
  if (!(g.s instanceof ListTerm)) return [];
3477
3562
  const nTerm = new Literal(String(g.s.elems.length));
3563
+
3564
+ const o2 = applySubstTerm(g.o, subst);
3565
+ if (isGroundTerm(o2)) {
3566
+ return termsEqualNoIntDecimal(o2, nTerm) ? [{ ...subst }] : [];
3567
+ }
3568
+
3478
3569
  const s2 = unifyTerm(g.o, nTerm, subst);
3479
3570
  return s2 !== null ? [s2] : [];
3480
3571
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [