eyeling 1.6.2 → 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 +430 -213
  2. package/package.json +1 -1
package/eyeling.js CHANGED
@@ -1263,10 +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;
1267
- if (a instanceof Literal) return a.value === b.value;
1268
+
1269
+ if (a instanceof Literal) {
1270
+ if (a.value === b.value) return true;
1271
+
1272
+ // Plain "abc" == "abc"^^xsd:string (but not language-tagged strings)
1273
+ if (literalsEquivalentAsXsdString(a.value, b.value)) return true;
1274
+
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
+ }
1302
+ }
1303
+
1304
+ return false;
1305
+ }
1306
+
1268
1307
  if (a instanceof Var) return a.name === b.name;
1269
1308
  if (a instanceof Blank) return a.label === b.label;
1309
+
1270
1310
  if (a instanceof ListTerm) {
1271
1311
  if (a.elems.length !== b.elems.length) return false;
1272
1312
  for (let i = 0; i < a.elems.length; i++) {
@@ -1274,6 +1314,7 @@ function termsEqual(a, b) {
1274
1314
  }
1275
1315
  return true;
1276
1316
  }
1317
+
1277
1318
  if (a instanceof OpenListTerm) {
1278
1319
  if (a.tailVar !== b.tailVar) return false;
1279
1320
  if (a.prefix.length !== b.prefix.length) return false;
@@ -1282,9 +1323,79 @@ function termsEqual(a, b) {
1282
1323
  }
1283
1324
  return true;
1284
1325
  }
1326
+
1285
1327
  if (a instanceof FormulaTerm) {
1286
1328
  return alphaEqFormulaTriples(a.triples, b.triples);
1287
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
+
1288
1399
  return false;
1289
1400
  }
1290
1401
 
@@ -1447,7 +1558,7 @@ function hasAlphaEquiv(triples, tr) {
1447
1558
 
1448
1559
  function termFastKey(t) {
1449
1560
  if (t instanceof Iri) return 'I:' + t.value;
1450
- if (t instanceof Literal) return 'L:' + t.value;
1561
+ if (t instanceof Literal) return 'L:' + normalizeLiteralForFastKey(t.value);
1451
1562
  return null;
1452
1563
  }
1453
1564
 
@@ -1882,18 +1993,40 @@ function unifyTerm(a, b, subst) {
1882
1993
  if (a instanceof Literal && b instanceof Literal && a.value === b.value) return { ...subst };
1883
1994
  if (a instanceof Blank && b instanceof Blank && a.label === b.label) return { ...subst };
1884
1995
 
1885
- // Numeric-value match for literals (EYE-style): allow different lexical forms / typing
1886
- // e.g. "1.1"^^xsd:double ≈ 1.1e0
1996
+ // String-literal match (RDF 1.1): treat plain strings and xsd:string as equal (but not @lang)
1887
1997
  if (a instanceof Literal && b instanceof Literal) {
1888
- const av = parseNumberLiteral(a); // BigInt | Number | null
1889
- const bv = parseNumberLiteral(b);
1890
- if (av !== null && bv !== null) {
1891
- if (typeof av === 'bigint' && typeof bv === 'bigint') {
1892
- if (av === bv) return { ...subst };
1893
- } else {
1894
- const an = typeof av === 'bigint' ? Number(av) : av;
1895
- const bn = typeof bv === 'bigint' ? Number(bv) : bv;
1896
- if (!Number.isNaN(an) && !Number.isNaN(bn) && an === bn) return { ...subst };
1998
+ if (literalsEquivalentAsXsdString(a.value, b.value)) return { ...subst };
1999
+ }
2000
+
2001
+ // Numeric-value match for literals, BUT ONLY when datatypes agree (or infer to agree)
2002
+ if (a instanceof Literal && b instanceof Literal) {
2003
+ const ai = parseNumericLiteralInfo(a);
2004
+ const bi = parseNumericLiteralInfo(b);
2005
+
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
+ }
1897
2030
  }
1898
2031
  }
1899
2032
  }
@@ -2000,6 +2133,65 @@ function literalParts(lit) {
2000
2133
  return [lex, dt];
2001
2134
  }
2002
2135
 
2136
+ function literalHasLangTag(lit) {
2137
+ // True iff the literal is a quoted string literal with a language tag suffix,
2138
+ // e.g. "hello"@en or """hello"""@en.
2139
+ // (The parser rejects language tags combined with datatypes.)
2140
+ if (typeof lit !== 'string') return false;
2141
+ if (lit.indexOf('^^') >= 0) return false;
2142
+ if (!lit.startsWith('"')) return false;
2143
+
2144
+ if (lit.startsWith('"""')) {
2145
+ const end = lit.lastIndexOf('"""');
2146
+ if (end < 0) return false;
2147
+ const after = end + 3;
2148
+ return after < lit.length && lit[after] === '@';
2149
+ }
2150
+
2151
+ const lastQuote = lit.lastIndexOf('"');
2152
+ if (lastQuote < 0) return false;
2153
+ const after = lastQuote + 1;
2154
+ return after < lit.length && lit[after] === '@';
2155
+ }
2156
+
2157
+ function isPlainStringLiteralValue(lit) {
2158
+ // Plain string literal: quoted, no datatype, no lang.
2159
+ if (typeof lit !== 'string') return false;
2160
+ if (lit.indexOf('^^') >= 0) return false;
2161
+ if (!isQuotedLexical(lit)) return false;
2162
+ return !literalHasLangTag(lit);
2163
+ }
2164
+
2165
+ function literalsEquivalentAsXsdString(aLit, bLit) {
2166
+ // Treat "abc" and "abc"^^xsd:string as equal, but do NOT conflate language-tagged strings.
2167
+ if (typeof aLit !== 'string' || typeof bLit !== 'string') return false;
2168
+
2169
+ const [alex, adt] = literalParts(aLit);
2170
+ const [blex, bdt] = literalParts(bLit);
2171
+ if (alex !== blex) return false;
2172
+
2173
+ const aPlain = adt === null && isPlainStringLiteralValue(aLit);
2174
+ const bPlain = bdt === null && isPlainStringLiteralValue(bLit);
2175
+ const aXsdStr = adt === XSD_NS + 'string';
2176
+ const bXsdStr = bdt === XSD_NS + 'string';
2177
+
2178
+ return (aPlain && bXsdStr) || (bPlain && aXsdStr);
2179
+ }
2180
+
2181
+ function normalizeLiteralForFastKey(lit) {
2182
+ // Canonicalize so that "abc" and "abc"^^xsd:string share the same index/dedup key.
2183
+ if (typeof lit !== 'string') return lit;
2184
+ const [lex, dt] = literalParts(lit);
2185
+
2186
+ if (dt === XSD_NS + 'string') {
2187
+ return `${lex}^^<${XSD_NS}string>`;
2188
+ }
2189
+ if (dt === null && isPlainStringLiteralValue(lit)) {
2190
+ return `${lex}^^<${XSD_NS}string>`;
2191
+ }
2192
+ return lit;
2193
+ }
2194
+
2003
2195
  function stripQuotes(lex) {
2004
2196
  if (lex.length >= 6 && lex.startsWith('"""') && lex.endsWith('"""')) {
2005
2197
  return lex.slice(3, -3);
@@ -2310,6 +2502,44 @@ function formatNum(n) {
2310
2502
  return String(n);
2311
2503
  }
2312
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
+
2313
2543
  function parseXsdDateTerm(t) {
2314
2544
  if (!(t instanceof Literal)) return null;
2315
2545
  const [lex, dt] = literalParts(t.value);
@@ -2478,6 +2708,90 @@ function numEqualTerm(t, n, eps = 1e-9) {
2478
2708
  return v !== null && Math.abs(v - n) < eps;
2479
2709
  }
2480
2710
 
2711
+ function numericDatatypeFromLex(lex) {
2712
+ if (/[eE]/.test(lex)) return XSD_NS + 'double';
2713
+ if (lex.includes('.')) return XSD_NS + 'decimal';
2714
+ return XSD_NS + 'integer';
2715
+ }
2716
+
2717
+ function parseNumericLiteralInfo(t) {
2718
+ if (!(t instanceof Literal)) return null;
2719
+
2720
+ const v = t.value;
2721
+ const [lex, dt] = literalParts(v);
2722
+
2723
+ let dt2 = dt;
2724
+ let lexStr;
2725
+
2726
+ if (dt2 !== null) {
2727
+ if (dt2 !== XSD_NS + 'integer' && dt2 !== XSD_NS + 'decimal' && dt2 !== XSD_NS + 'double') return null;
2728
+ lexStr = stripQuotes(lex);
2729
+ } else {
2730
+ if (typeof v !== 'string') return null;
2731
+ if (v.startsWith('"')) return null; // exclude quoted strings
2732
+ if (!/^[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?$/.test(v)) return null;
2733
+
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
+
2738
+ lexStr = v;
2739
+ }
2740
+
2741
+ if (dt2 === XSD_NS + 'integer') {
2742
+ try {
2743
+ return { dt: dt2, kind: 'bigint', value: BigInt(lexStr), lexStr };
2744
+ } catch {
2745
+ return null;
2746
+ }
2747
+ }
2748
+
2749
+ const num = Number(lexStr);
2750
+ if (Number.isNaN(num)) return null;
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 [];
2793
+ }
2794
+
2481
2795
  // ============================================================================
2482
2796
  // Backward proof & builtins mutual recursion — declarations first
2483
2797
  // ============================================================================
@@ -2867,28 +3181,6 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
2867
3181
  }
2868
3182
  }
2869
3183
 
2870
- // math:negation
2871
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'negation') {
2872
- const a = parseNum(g.s);
2873
- if (a !== null && g.o instanceof Var) {
2874
- const s2 = { ...subst };
2875
- s2[g.o.name] = new Literal(formatNum(-a));
2876
- return [s2];
2877
- }
2878
- const b = parseNum(g.o);
2879
- if (g.s instanceof Var && b !== null) {
2880
- const s2 = { ...subst };
2881
- s2[g.s.name] = new Literal(formatNum(-b));
2882
- return [s2];
2883
- }
2884
- const a2 = parseNum(g.s);
2885
- const b2 = parseNum(g.o);
2886
- if (a2 !== null && b2 !== null) {
2887
- if (Math.abs(-a2 - b2) < 1e-9) return [{ ...subst }];
2888
- }
2889
- return [];
2890
- }
2891
-
2892
3184
  // math:absoluteValue
2893
3185
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'absoluteValue') {
2894
3186
  const a = parseNum(g.s);
@@ -2904,135 +3196,61 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
2904
3196
  return [];
2905
3197
  }
2906
3198
 
2907
- // math:cos
2908
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'cos') {
2909
- const a = parseNum(g.s);
2910
- if (a !== null) {
2911
- const cVal = Math.cos(a);
2912
- if (g.o instanceof Var) {
2913
- const s2 = { ...subst };
2914
- s2[g.o.name] = new Literal(formatNum(cVal));
2915
- return [s2];
2916
- }
2917
- if (numEqualTerm(g.o, cVal)) {
2918
- return [{ ...subst }];
2919
- }
2920
- }
2921
- return [];
2922
- }
2923
-
2924
- // math:sin
2925
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sin') {
2926
- const a = parseNum(g.s);
2927
- if (a !== null) {
2928
- const cVal = Math.sin(a);
2929
- if (g.o instanceof Var) {
2930
- const s2 = { ...subst };
2931
- s2[g.o.name] = new Literal(formatNum(cVal));
2932
- return [s2];
2933
- }
2934
- if (numEqualTerm(g.o, cVal)) {
2935
- return [{ ...subst }];
2936
- }
2937
- }
2938
- return [];
2939
- }
2940
-
2941
3199
  // math:acos
2942
3200
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'acos') {
2943
- const a = parseNum(g.s);
2944
- if (a !== null) {
2945
- const cVal = Math.acos(a);
2946
- if (Number.isFinite(cVal)) {
2947
- if (g.o instanceof Var) {
2948
- const s2 = { ...subst };
2949
- s2[g.o.name] = new Literal(formatNum(cVal));
2950
- return [s2];
2951
- }
2952
- if (numEqualTerm(g.o, cVal)) {
2953
- return [{ ...subst }];
2954
- }
2955
- }
2956
- }
2957
- return [];
3201
+ return evalUnaryMathRel(g, subst, Math.acos, Math.cos);
2958
3202
  }
2959
3203
 
2960
3204
  // math:asin
2961
3205
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'asin') {
2962
- const a = parseNum(g.s);
2963
- if (a !== null) {
2964
- const cVal = Math.asin(a);
2965
- if (Number.isFinite(cVal)) {
2966
- if (g.o instanceof Var) {
2967
- const s2 = { ...subst };
2968
- s2[g.o.name] = new Literal(formatNum(cVal));
2969
- return [s2];
2970
- }
2971
- if (numEqualTerm(g.o, cVal)) {
2972
- return [{ ...subst }];
2973
- }
2974
- }
2975
- }
2976
- return [];
3206
+ return evalUnaryMathRel(g, subst, Math.asin, Math.sin);
2977
3207
  }
2978
3208
 
2979
3209
  // math:atan
2980
3210
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'atan') {
2981
- const a = parseNum(g.s);
2982
- if (a !== null) {
2983
- const cVal = Math.atan(a);
2984
- if (Number.isFinite(cVal)) {
2985
- if (g.o instanceof Var) {
2986
- const s2 = { ...subst };
2987
- s2[g.o.name] = new Literal(formatNum(cVal));
2988
- return [s2];
2989
- }
2990
- if (numEqualTerm(g.o, cVal)) {
2991
- return [{ ...subst }];
2992
- }
2993
- }
2994
- }
2995
- return [];
3211
+ return evalUnaryMathRel(g, subst, Math.atan, Math.tan);
3212
+ }
3213
+
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);
2996
3217
  }
2997
3218
 
2998
- // math:cosh
2999
- // Hyperbolic cosine
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
+ }
3000
3234
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'cosh') {
3001
- const a = parseNum(g.s);
3002
- if (a !== null && typeof Math.cosh === 'function') {
3003
- const cVal = Math.cosh(a);
3004
- if (Number.isFinite(cVal)) {
3005
- if (g.o instanceof Var) {
3006
- const s2 = { ...subst };
3007
- s2[g.o.name] = new Literal(formatNum(cVal));
3008
- return [s2];
3009
- }
3010
- if (numEqualTerm(g.o, cVal)) {
3011
- return [{ ...subst }];
3012
- }
3013
- }
3014
- }
3015
- 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);
3016
3241
  }
3017
3242
 
3018
- // math:degrees
3019
- // Convert radians -> degrees
3243
+ // math:degrees (inverse is radians)
3020
3244
  if (g.p instanceof Iri && g.p.value === MATH_NS + 'degrees') {
3021
- const a = parseNum(g.s);
3022
- if (a !== null) {
3023
- const cVal = (a * 180.0) / Math.PI;
3024
- if (Number.isFinite(cVal)) {
3025
- if (g.o instanceof Var) {
3026
- const s2 = { ...subst };
3027
- s2[g.o.name] = new Literal(formatNum(cVal));
3028
- return [s2];
3029
- }
3030
- if (numEqualTerm(g.o, cVal)) {
3031
- return [{ ...subst }];
3032
- }
3033
- }
3034
- }
3035
- 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);
3036
3254
  }
3037
3255
 
3038
3256
  // math:remainder
@@ -3075,63 +3293,6 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3075
3293
  return s2 !== null ? [s2] : [];
3076
3294
  }
3077
3295
 
3078
- // math:sinh
3079
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'sinh') {
3080
- const a = parseNum(g.s);
3081
- if (a !== null && typeof Math.sinh === 'function') {
3082
- const cVal = Math.sinh(a);
3083
- if (Number.isFinite(cVal)) {
3084
- if (g.o instanceof Var) {
3085
- const s2 = { ...subst };
3086
- s2[g.o.name] = new Literal(formatNum(cVal));
3087
- return [s2];
3088
- }
3089
- if (numEqualTerm(g.o, cVal)) {
3090
- return [{ ...subst }];
3091
- }
3092
- }
3093
- }
3094
- return [];
3095
- }
3096
-
3097
- // math:tan
3098
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tan') {
3099
- const a = parseNum(g.s);
3100
- if (a !== null) {
3101
- const cVal = Math.tan(a);
3102
- if (Number.isFinite(cVal)) {
3103
- if (g.o instanceof Var) {
3104
- const s2 = { ...subst };
3105
- s2[g.o.name] = new Literal(formatNum(cVal));
3106
- return [s2];
3107
- }
3108
- if (numEqualTerm(g.o, cVal)) {
3109
- return [{ ...subst }];
3110
- }
3111
- }
3112
- }
3113
- return [];
3114
- }
3115
-
3116
- // math:tanh
3117
- if (g.p instanceof Iri && g.p.value === MATH_NS + 'tanh') {
3118
- const a = parseNum(g.s);
3119
- if (a !== null && typeof Math.tanh === 'function') {
3120
- const cVal = Math.tanh(a);
3121
- if (Number.isFinite(cVal)) {
3122
- if (g.o instanceof Var) {
3123
- const s2 = { ...subst };
3124
- s2[g.o.name] = new Literal(formatNum(cVal));
3125
- return [s2];
3126
- }
3127
- if (numEqualTerm(g.o, cVal)) {
3128
- return [{ ...subst }];
3129
- }
3130
- }
3131
- }
3132
- return [];
3133
- }
3134
-
3135
3296
  // -----------------------------------------------------------------
3136
3297
  // 4.3 time: builtins
3137
3298
  // -----------------------------------------------------------------
@@ -3264,12 +3425,37 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3264
3425
  if (!(g.s instanceof ListTerm)) return [];
3265
3426
  const xs = g.s.elems;
3266
3427
  const outs = [];
3428
+
3267
3429
  for (let i = 0; i < xs.length; i++) {
3268
3430
  const idxLit = new Literal(String(i)); // 0-based
3269
- 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]);
3270
3455
  const s2 = unifyTerm(g.o, pair, subst);
3271
3456
  if (s2 !== null) outs.push(s2);
3272
3457
  }
3458
+
3273
3459
  return outs;
3274
3460
  }
3275
3461
 
@@ -3292,17 +3478,35 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3292
3478
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3293
3479
  const [listTerm, indexTerm] = g.s.elems;
3294
3480
  if (!(listTerm instanceof ListTerm)) return [];
3481
+
3295
3482
  const xs = listTerm.elems;
3296
3483
  const outs = [];
3297
3484
 
3298
3485
  for (let i = 0; i < xs.length; i++) {
3299
3486
  const idxLit = new Literal(String(i)); // index starts at 0
3300
- let s1 = unifyTerm(indexTerm, idxLit, subst);
3301
- if (s1 === null) continue;
3302
- let s2 = unifyTerm(g.o, xs[i], s1);
3303
- if (s2 === null) continue;
3304
- 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);
3305
3508
  }
3509
+
3306
3510
  return outs;
3307
3511
  }
3308
3512
 
@@ -3313,11 +3517,18 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3313
3517
  if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
3314
3518
  const [listTerm, itemTerm] = g.s.elems;
3315
3519
  if (!(listTerm instanceof ListTerm)) return [];
3520
+
3521
+ // item must be bound
3522
+ const item2 = applySubstTerm(itemTerm, subst);
3523
+ if (!isGroundTerm(item2)) return [];
3524
+
3316
3525
  const xs = listTerm.elems;
3317
3526
  const filtered = [];
3318
3527
  for (const e of xs) {
3319
- 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);
3320
3530
  }
3531
+
3321
3532
  const resList = new ListTerm(filtered);
3322
3533
  const s2 = unifyTerm(g.o, resList, subst);
3323
3534
  return s2 !== null ? [s2] : [];
@@ -3345,10 +3556,16 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen) {
3345
3556
  return outs;
3346
3557
  }
3347
3558
 
3348
- // list:length
3559
+ // list:length (strict: do not accept integer<->decimal matches for a ground object)
3349
3560
  if (g.p instanceof Iri && g.p.value === LIST_NS + 'length') {
3350
3561
  if (!(g.s instanceof ListTerm)) return [];
3351
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
+
3352
3569
  const s2 = unifyTerm(g.o, nTerm, subst);
3353
3570
  return s2 !== null ? [s2] : [];
3354
3571
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [