eyeling 1.25.2 → 1.25.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.
package/lib/cli.js CHANGED
@@ -350,12 +350,14 @@ function main() {
350
350
  outTriples = res.queryTriples;
351
351
  outDerived = res.queryDerived;
352
352
  } else {
353
+ const skipDerivedCollection = mayAutoRenderOutputStrings && !engine.getProofCommentsEnabled();
353
354
  derived = engine.forwardChain(facts, frules, brules, null, {
354
355
  captureExplanations: engine.getProofCommentsEnabled(),
356
+ collectDerived: !skipDerivedCollection,
355
357
  prefixes,
356
358
  });
357
359
  outDerived = derived;
358
- outTriples = derived.map((df) => df.fact);
360
+ outTriples = skipDerivedCollection ? [] : derived.map((df) => df.fact);
359
361
  }
360
362
 
361
363
  const renderedOutputTriples = hasQueries ? outTriples : facts;
package/lib/engine.js CHANGED
@@ -1034,6 +1034,12 @@ function alphaEqGraphTriples(xs, ys, opts) {
1034
1034
  // - __byPred: Map<predicateId, number[]> (indices into facts array)
1035
1035
  // - __byPS: Map<predicateId, Map<subjectId, number[]>>
1036
1036
  // - __byPO: Map<predicateId, Map<objectId, number[]>>
1037
+ // - __byPNonFastS / __byPNonFastO: Map<predicateId, number[]>
1038
+ // IRI-predicate facts whose subject/object cannot be indexed by fast key.
1039
+ // These are the fallback clauses for a constrained subject/object, just
1040
+ // like a Prolog clause index keeps variable-headed clauses as fallback.
1041
+ // - __varPred* indexes: facts whose predicate is a Var. Only these non-IRI
1042
+ // predicate facts can unify with a ground IRI predicate goal.
1037
1043
  // - __keySet: Set<"S\tP\tO"> for Iri/Literal/Blank-only triples (fast dup check)
1038
1044
  //
1039
1045
  // Backward rules:
@@ -1049,6 +1055,8 @@ const __compoundKeyToTid = new Map();
1049
1055
  // Use a negative id space so we never collide with __tid (which is positive).
1050
1056
  let __nextCompoundTid = -1;
1051
1057
 
1058
+ const EMPTY_FACT_INDEX_BUCKET = Object.freeze([]);
1059
+
1052
1060
  function __internCompoundTid(key) {
1053
1061
  const hit = __compoundKeyToTid.get(key);
1054
1062
  if (hit !== undefined) return hit;
@@ -1100,6 +1108,71 @@ function termFastKey(t) {
1100
1108
  return null;
1101
1109
  }
1102
1110
 
1111
+ function encodeLookupKeyPart(k) {
1112
+ if (typeof k === 'number') return 'T' + k;
1113
+ const s = String(k);
1114
+ return 'K' + s.length + ':' + s;
1115
+ }
1116
+
1117
+ function literalLookupKey(t) {
1118
+ const boolInfo = parseBooleanLiteralInfo(t);
1119
+ if (boolInfo) return '\u0000B' + (boolInfo.value ? '1' : '0');
1120
+
1121
+ const numInfo = parseNumericLiteralInfo(t);
1122
+ if (numInfo) {
1123
+ if (numInfo.kind === 'bigint') return '\u0000N' + numInfo.dt + '\u0000' + numInfo.value.toString();
1124
+
1125
+ const n = numInfo.value;
1126
+ // Normal unification intentionally does not make NaN value-equal to NaN;
1127
+ // only identical lexical literals match through the ordinary __tid path.
1128
+ if (!Number.isNaN(n)) return '\u0000N' + numInfo.dt + '\u0000' + String(n);
1129
+ }
1130
+
1131
+ // Covers exact literals plus plain string / xsd:string canonicalization, which
1132
+ // Literal construction already normalizes into a shared __tid.
1133
+ return termFastKey(t);
1134
+ }
1135
+
1136
+ function termLookupKey(t) {
1137
+ // Lookup keys summarize the equality accepted by ordinary unifyTerm(), not
1138
+ // merely object identity. This keeps literal-index pruning complete for
1139
+ // value-equivalent booleans/numerics such as true/"1"^^xsd:boolean and
1140
+ // 1.0/1.00, while preserving exact fast ids for IRIs, blanks, and strings.
1141
+ if (t instanceof Iri) {
1142
+ if (t.value === RDF_NIL_IRI) return '\u0000L0';
1143
+ return t.__tid;
1144
+ }
1145
+ if (t instanceof Blank) return t.__tid;
1146
+ if (t instanceof Literal) return literalLookupKey(t);
1147
+
1148
+ if (t instanceof ListTerm) {
1149
+ const cached = t.__lookupKey;
1150
+ if (cached !== undefined) return cached === false ? null : cached;
1151
+
1152
+ const xs = t.elems;
1153
+ if (xs.length === 0) {
1154
+ Object.defineProperty(t, '__lookupKey', { value: '\u0000L0', enumerable: false });
1155
+ return '\u0000L0';
1156
+ }
1157
+
1158
+ const parts = new Array(xs.length);
1159
+ for (let i = 0; i < xs.length; i++) {
1160
+ const k = termLookupKey(xs[i]);
1161
+ if (k === null) {
1162
+ Object.defineProperty(t, '__lookupKey', { value: false, enumerable: false });
1163
+ return null;
1164
+ }
1165
+ parts[i] = encodeLookupKeyPart(k);
1166
+ }
1167
+
1168
+ const key = '\u0000L' + xs.length + '\u0001' + parts.join('\u0001');
1169
+ Object.defineProperty(t, '__lookupKey', { value: key, enumerable: false });
1170
+ return key;
1171
+ }
1172
+
1173
+ return null;
1174
+ }
1175
+
1103
1176
  function tripleFastKey(tr) {
1104
1177
  const ks = termFastKey(tr.s);
1105
1178
  const kp = termFastKey(tr.p);
@@ -1113,9 +1186,13 @@ function ensureFactIndexes(facts) {
1113
1186
  facts.__byPred &&
1114
1187
  facts.__byPS &&
1115
1188
  facts.__byPO &&
1116
- facts.__wildPred &&
1117
- facts.__wildPS &&
1118
- facts.__wildPO &&
1189
+ facts.__byPNonFastS &&
1190
+ facts.__byPNonFastO &&
1191
+ facts.__varPred &&
1192
+ facts.__varPredPS &&
1193
+ facts.__varPredPO &&
1194
+ facts.__varPredNonFastS &&
1195
+ facts.__varPredNonFastO &&
1119
1196
  facts.__keySet
1120
1197
  )
1121
1198
  return;
@@ -1135,21 +1212,41 @@ function ensureFactIndexes(facts) {
1135
1212
  enumerable: false,
1136
1213
  writable: true,
1137
1214
  });
1138
- Object.defineProperty(facts, '__wildPred', {
1215
+ Object.defineProperty(facts, '__byPNonFastS', {
1216
+ value: new Map(),
1217
+ enumerable: false,
1218
+ writable: true,
1219
+ });
1220
+ Object.defineProperty(facts, '__byPNonFastO', {
1221
+ value: new Map(),
1222
+ enumerable: false,
1223
+ writable: true,
1224
+ });
1225
+ Object.defineProperty(facts, '__varPred', {
1139
1226
  value: [],
1140
1227
  enumerable: false,
1141
1228
  writable: true,
1142
1229
  });
1143
- Object.defineProperty(facts, '__wildPS', {
1230
+ Object.defineProperty(facts, '__varPredPS', {
1144
1231
  value: new Map(),
1145
1232
  enumerable: false,
1146
1233
  writable: true,
1147
1234
  });
1148
- Object.defineProperty(facts, '__wildPO', {
1235
+ Object.defineProperty(facts, '__varPredPO', {
1149
1236
  value: new Map(),
1150
1237
  enumerable: false,
1151
1238
  writable: true,
1152
1239
  });
1240
+ Object.defineProperty(facts, '__varPredNonFastS', {
1241
+ value: [],
1242
+ enumerable: false,
1243
+ writable: true,
1244
+ });
1245
+ Object.defineProperty(facts, '__varPredNonFastO', {
1246
+ value: [],
1247
+ enumerable: false,
1248
+ writable: true,
1249
+ });
1153
1250
  Object.defineProperty(facts, '__keySet', {
1154
1251
  value: new Set(),
1155
1252
  enumerable: false,
@@ -1190,16 +1287,53 @@ function cloneFactIndexesForSnapshot(src, dest) {
1190
1287
  Object.defineProperty(dest, '__byPred', { value: cloneArrayMap(src.__byPred), enumerable: false, writable: true });
1191
1288
  Object.defineProperty(dest, '__byPS', { value: cloneNestedArrayMap(src.__byPS), enumerable: false, writable: true });
1192
1289
  Object.defineProperty(dest, '__byPO', { value: cloneNestedArrayMap(src.__byPO), enumerable: false, writable: true });
1193
- Object.defineProperty(dest, '__wildPred', { value: src.__wildPred.slice(), enumerable: false, writable: true });
1194
- Object.defineProperty(dest, '__wildPS', { value: cloneArrayMap(src.__wildPS), enumerable: false, writable: true });
1195
- Object.defineProperty(dest, '__wildPO', { value: cloneArrayMap(src.__wildPO), enumerable: false, writable: true });
1290
+ Object.defineProperty(dest, '__byPNonFastS', {
1291
+ value: cloneArrayMap(src.__byPNonFastS),
1292
+ enumerable: false,
1293
+ writable: true,
1294
+ });
1295
+ Object.defineProperty(dest, '__byPNonFastO', {
1296
+ value: cloneArrayMap(src.__byPNonFastO),
1297
+ enumerable: false,
1298
+ writable: true,
1299
+ });
1300
+ Object.defineProperty(dest, '__varPred', { value: src.__varPred.slice(), enumerable: false, writable: true });
1301
+ Object.defineProperty(dest, '__varPredPS', {
1302
+ value: cloneArrayMap(src.__varPredPS),
1303
+ enumerable: false,
1304
+ writable: true,
1305
+ });
1306
+ Object.defineProperty(dest, '__varPredPO', {
1307
+ value: cloneArrayMap(src.__varPredPO),
1308
+ enumerable: false,
1309
+ writable: true,
1310
+ });
1311
+ Object.defineProperty(dest, '__varPredNonFastS', {
1312
+ value: src.__varPredNonFastS.slice(),
1313
+ enumerable: false,
1314
+ writable: true,
1315
+ });
1316
+ Object.defineProperty(dest, '__varPredNonFastO', {
1317
+ value: src.__varPredNonFastO.slice(),
1318
+ enumerable: false,
1319
+ writable: true,
1320
+ });
1196
1321
  Object.defineProperty(dest, '__keySet', { value: new Set(src.__keySet), enumerable: false, writable: true });
1197
1322
  Object.defineProperty(dest, '__keySetComplete', { value: !!src.__keySetComplete, enumerable: false, writable: true });
1198
1323
  }
1199
1324
 
1325
+ function addToIndexArrayMap(map, key, value) {
1326
+ let bucket = map.get(key);
1327
+ if (!bucket) {
1328
+ bucket = [];
1329
+ map.set(key, bucket);
1330
+ }
1331
+ bucket.push(value);
1332
+ }
1333
+
1200
1334
  function indexFact(facts, tr, idx, addKeySet = true) {
1201
- const sk = termFastKey(tr.s);
1202
- const ok = termFastKey(tr.o);
1335
+ const sk = termLookupKey(tr.s);
1336
+ const ok = termLookupKey(tr.o);
1203
1337
  let pkForKey = null;
1204
1338
 
1205
1339
  if (tr.p instanceof Iri) {
@@ -1220,12 +1354,9 @@ function indexFact(facts, tr, idx, addKeySet = true) {
1220
1354
  ps = new Map();
1221
1355
  facts.__byPS.set(pk, ps);
1222
1356
  }
1223
- let psb = ps.get(sk);
1224
- if (!psb) {
1225
- psb = [];
1226
- ps.set(sk, psb);
1227
- }
1228
- psb.push(idx);
1357
+ addToIndexArrayMap(ps, sk, idx);
1358
+ } else {
1359
+ addToIndexArrayMap(facts.__byPNonFastS, pk, idx);
1229
1360
  }
1230
1361
 
1231
1362
  if (ok !== null) {
@@ -1234,32 +1365,23 @@ function indexFact(facts, tr, idx, addKeySet = true) {
1234
1365
  po = new Map();
1235
1366
  facts.__byPO.set(pk, po);
1236
1367
  }
1237
- let pob = po.get(ok);
1238
- if (!pob) {
1239
- pob = [];
1240
- po.set(ok, pob);
1241
- }
1242
- pob.push(idx);
1368
+ addToIndexArrayMap(po, ok, idx);
1369
+ } else {
1370
+ addToIndexArrayMap(facts.__byPNonFastO, pk, idx);
1243
1371
  }
1244
- } else {
1245
- facts.__wildPred.push(idx);
1372
+ } else if (tr.p instanceof Var) {
1373
+ facts.__varPred.push(idx);
1246
1374
 
1247
1375
  if (sk !== null) {
1248
- let psb = facts.__wildPS.get(sk);
1249
- if (!psb) {
1250
- psb = [];
1251
- facts.__wildPS.set(sk, psb);
1252
- }
1253
- psb.push(idx);
1376
+ addToIndexArrayMap(facts.__varPredPS, sk, idx);
1377
+ } else {
1378
+ facts.__varPredNonFastS.push(idx);
1254
1379
  }
1255
1380
 
1256
1381
  if (ok !== null) {
1257
- let pob = facts.__wildPO.get(ok);
1258
- if (!pob) {
1259
- pob = [];
1260
- facts.__wildPO.set(ok, pob);
1261
- }
1262
- pob.push(idx);
1382
+ addToIndexArrayMap(facts.__varPredPO, ok, idx);
1383
+ } else {
1384
+ facts.__varPredNonFastO.push(idx);
1263
1385
  }
1264
1386
  }
1265
1387
 
@@ -1269,55 +1391,86 @@ function indexFact(facts, tr, idx, addKeySet = true) {
1269
1391
  }
1270
1392
  }
1271
1393
 
1394
+ function mergeIndexBuckets(primary, fallback) {
1395
+ const a = primary && primary.length ? primary : null;
1396
+ const b = fallback && fallback.length ? fallback : null;
1397
+ if (!a && !b) return EMPTY_FACT_INDEX_BUCKET;
1398
+ if (!a) return b;
1399
+ if (!b) return a;
1400
+ const out = new Array(a.length + b.length);
1401
+ for (let i = 0; i < a.length; i++) out[i] = a[i];
1402
+ for (let i = 0; i < b.length; i++) out[a.length + i] = b[i];
1403
+ return out;
1404
+ }
1405
+
1406
+ function selectPositionIndexedCandidates(all, exactByS, fallbackS, sk, exactByO, fallbackO, ok) {
1407
+ if (sk === null && ok === null) return all && all.length ? all : EMPTY_FACT_INDEX_BUCKET;
1408
+
1409
+ let sBucket = null;
1410
+ if (sk !== null) sBucket = mergeIndexBuckets(exactByS || null, fallbackS || null);
1411
+
1412
+ let oBucket = null;
1413
+ if (ok !== null) oBucket = mergeIndexBuckets(exactByO || null, fallbackO || null);
1414
+
1415
+ if (sk !== null && ok !== null) return sBucket.length <= oBucket.length ? sBucket : oBucket;
1416
+ return sk !== null ? sBucket : oBucket;
1417
+ }
1418
+
1272
1419
  function candidateFacts(facts, goal) {
1273
1420
  ensureFactIndexes(facts);
1274
1421
 
1275
1422
  if (goal.p instanceof Iri) {
1276
1423
  const pk = goal.p.__tid;
1277
1424
 
1278
- const sk = termFastKey(goal.s);
1279
- const ok = termFastKey(goal.o);
1425
+ const sk = termLookupKey(goal.s);
1426
+ const ok = termLookupKey(goal.o);
1280
1427
 
1281
- /** @type {number[] | null} */
1282
1428
  let byPS = null;
1283
1429
  if (sk !== null) {
1284
1430
  const ps = facts.__byPS.get(pk);
1285
1431
  if (ps) byPS = ps.get(sk) || null;
1286
1432
  }
1433
+ const byPNonFastS = sk !== null ? facts.__byPNonFastS.get(pk) || null : null;
1287
1434
 
1288
- /** @type {number[] | null} */
1289
1435
  let byPO = null;
1290
1436
  if (ok !== null) {
1291
1437
  const po = facts.__byPO.get(pk);
1292
1438
  if (po) byPO = po.get(ok) || null;
1293
1439
  }
1440
+ const byPNonFastO = ok !== null ? facts.__byPNonFastO.get(pk) || null : null;
1294
1441
 
1295
- let exact = null;
1296
- if (byPS && byPO) exact = byPS.length <= byPO.length ? byPS : byPO;
1297
- else if (byPS) exact = byPS;
1298
- else if (byPO) exact = byPO;
1299
- else exact = facts.__byPred.get(pk) || null;
1442
+ const exact = selectPositionIndexedCandidates(
1443
+ facts.__byPred.get(pk) || null,
1444
+ byPS,
1445
+ byPNonFastS,
1446
+ sk,
1447
+ byPO,
1448
+ byPNonFastO,
1449
+ ok,
1450
+ );
1300
1451
 
1301
- /** @type {number[] | null} */
1302
- let wildPS = null;
1303
- if (sk !== null) wildPS = facts.__wildPS.get(sk) || null;
1452
+ let varPredPS = null;
1453
+ if (sk !== null) varPredPS = facts.__varPredPS.get(sk) || null;
1304
1454
 
1305
- /** @type {number[] | null} */
1306
- let wildPO = null;
1307
- if (ok !== null) wildPO = facts.__wildPO.get(ok) || null;
1455
+ let varPredPO = null;
1456
+ if (ok !== null) varPredPO = facts.__varPredPO.get(ok) || null;
1308
1457
 
1309
- let wild = null;
1310
- if (wildPS && wildPO) wild = wildPS.length <= wildPO.length ? wildPS : wildPO;
1311
- else if (wildPS) wild = wildPS;
1312
- else if (wildPO) wild = wildPO;
1313
- else wild = facts.__wildPred.length ? facts.__wildPred : null;
1458
+ const wild = selectPositionIndexedCandidates(
1459
+ facts.__varPred,
1460
+ varPredPS,
1461
+ sk !== null ? facts.__varPredNonFastS : null,
1462
+ sk,
1463
+ varPredPO,
1464
+ ok !== null ? facts.__varPredNonFastO : null,
1465
+ ok,
1466
+ );
1314
1467
 
1315
1468
  return {
1316
- exact: exact || null,
1317
- wild: wild || null,
1318
- exactLen: exact ? exact.length : 0,
1319
- wildLen: wild ? wild.length : 0,
1320
- totalLen: (exact ? exact.length : 0) + (wild ? wild.length : 0),
1469
+ exact,
1470
+ wild,
1471
+ exactLen: exact.length,
1472
+ wildLen: wild.length,
1473
+ totalLen: exact.length + wild.length,
1321
1474
  };
1322
1475
  }
1323
1476
 
@@ -1335,20 +1488,28 @@ function hasFactIndexed(facts, tr) {
1335
1488
 
1336
1489
  if (tr.p instanceof Iri) {
1337
1490
  const pk = tr.p.__tid;
1491
+ const sk = termLookupKey(tr.s);
1492
+ let best = null;
1338
1493
 
1339
- const ok = termFastKey(tr.o);
1494
+ if (sk !== null) {
1495
+ const ps = facts.__byPS.get(pk);
1496
+ if (!ps) return false;
1497
+ const psb = ps.get(sk);
1498
+ if (!psb || psb.length === 0) return false;
1499
+ best = psb;
1500
+ }
1501
+
1502
+ const ok = termLookupKey(tr.o);
1340
1503
  if (ok !== null) {
1341
1504
  const po = facts.__byPO.get(pk);
1342
- if (po) {
1343
- const pob = po.get(ok) || [];
1344
- // Facts are all in the same graph. Different blank node labels represent
1345
- // different existentials unless explicitly connected. Do NOT treat
1346
- // triples as duplicates modulo blank renaming, or you'll incorrectly
1347
- // drop facts like: _:sk_0 :x 8.0 (because _:b8 :x 8.0 exists).
1348
- return pob.some((i) => triplesEqual(facts[i], tr));
1349
- }
1505
+ if (!po) return false;
1506
+ const pob = po.get(ok);
1507
+ if (!pob || pob.length === 0) return false;
1508
+ if (!best || pob.length < best.length) best = pob;
1350
1509
  }
1351
1510
 
1511
+ if (best) return best.some((i) => triplesEqual(facts[i], tr));
1512
+
1352
1513
  const pb = facts.__byPred.get(pk) || [];
1353
1514
  return pb.some((i) => triplesEqual(facts[i], tr));
1354
1515
  }
@@ -1457,11 +1618,25 @@ function mergeSinglePremiseAgendaBuckets() {
1457
1618
  return out;
1458
1619
  }
1459
1620
 
1621
+ function termContainsVarForAgenda(t) {
1622
+ if (t instanceof Var) return true;
1623
+ if (t instanceof ListTerm) return t.elems.some(termContainsVarForAgenda);
1624
+ if (t instanceof OpenListTerm) return true;
1625
+ if (t instanceof GraphTerm)
1626
+ return t.triples.some(
1627
+ (tr) =>
1628
+ termContainsVarForAgenda(tr.s) || termContainsVarForAgenda(tr.p) || termContainsVarForAgenda(tr.o),
1629
+ );
1630
+ return false;
1631
+ }
1632
+
1460
1633
  function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1461
1634
  const index = {
1462
1635
  byPred: new Map(),
1636
+ byPredAll: new Map(),
1463
1637
  byPS: new Map(),
1464
1638
  byPO: new Map(),
1639
+ allIriPred: [],
1465
1640
  wildPred: [],
1466
1641
  wildPS: new Map(),
1467
1642
  wildPO: new Map(),
@@ -1483,8 +1658,8 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1483
1658
  if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
1484
1659
 
1485
1660
  const goal = r.premise[0];
1486
- const goalSKey = termFastKey(goal.s);
1487
- const goalOKey = termFastKey(goal.o);
1661
+ const goalSKey = termLookupKey(goal.s);
1662
+ const goalOKey = termLookupKey(goal.o);
1488
1663
  const fastSubjectVar = goal.p instanceof Iri && goal.s instanceof Var && goalOKey !== null ? goal.s.name : null;
1489
1664
  const fastObjectVar = goal.p instanceof Iri && goal.o instanceof Var && goalSKey !== null ? goal.o.name : null;
1490
1665
  const entry = {
@@ -1503,6 +1678,8 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1503
1678
  index.size += 1;
1504
1679
 
1505
1680
  if (entry.goalPredTid !== null) {
1681
+ addToMapArray(index.byPredAll, entry.goalPredTid, entry);
1682
+ index.allIriPred.push(entry);
1506
1683
  if (entry.goalSKey === null && entry.goalOKey === null) addToMapArray(index.byPred, entry.goalPredTid, entry);
1507
1684
  if (entry.goalSKey !== null) {
1508
1685
  let ps = index.byPS.get(entry.goalPredTid);
@@ -1533,25 +1710,37 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1533
1710
  function getSinglePremiseAgendaCandidates(index, fact) {
1534
1711
  if (!index || index.size === 0) return null;
1535
1712
 
1536
- const sk = termFastKey(fact.s);
1537
- const ok = termFastKey(fact.o);
1713
+ const sk = termLookupKey(fact.s);
1714
+ const ok = termLookupKey(fact.o);
1538
1715
 
1539
1716
  let exact = null;
1540
1717
  if (fact.p instanceof Iri) {
1541
1718
  const pk = fact.p.__tid;
1542
- const byPred = index.byPred.get(pk) || null;
1543
- let byPS = null;
1544
- if (sk !== null) {
1719
+ if ((sk === null && termContainsVarForAgenda(fact.s)) || (ok === null && termContainsVarForAgenda(fact.o))) {
1720
+ // A fact with a variable-bearing subject/object (most importantly a
1721
+ // top-level variable fact such as `?S :p ?O.`) can match rules whose
1722
+ // premise is fixed in that position. The ordinary `(p,s)` / `(p,o)` lookup
1723
+ // would miss those rules, so fall back to all agenda-indexed rules for
1724
+ // this predicate. Do not do this merely for non-fast quoted formulas:
1725
+ // they are not wildcards, and broad fallback would over-fire rules that
1726
+ // rely on protected blank-node/formula unification semantics.
1727
+ exact = index.byPredAll.get(pk) || null;
1728
+ } else {
1729
+ const byPred = index.byPred.get(pk) || null;
1730
+ let byPS = null;
1545
1731
  const ps = index.byPS.get(pk);
1546
1732
  if (ps) byPS = ps.get(sk) || null;
1547
- }
1548
- let byPO = null;
1549
- if (ok !== null) {
1733
+ let byPO = null;
1550
1734
  const po = index.byPO.get(pk);
1551
1735
  if (po) byPO = po.get(ok) || null;
1552
- }
1553
1736
 
1554
- exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
1737
+ exact = mergeSinglePremiseAgendaBuckets(byPred, byPS, byPO);
1738
+ }
1739
+ } else if (fact.p instanceof Var) {
1740
+ // A variable-predicate fact can match any IRI-predicate agenda rule.
1741
+ // This is deliberately broad and relies on final unification below; such
1742
+ // facts are uncommon and correctness matters more than over-indexing them.
1743
+ exact = index.allIriPred.length ? index.allIriPred : null;
1555
1744
  }
1556
1745
 
1557
1746
  const wildPred = index.wildPred.length ? index.wildPred : null;
@@ -2901,6 +3090,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2901
3090
  __attachGoalTable(backRules, goalTable);
2902
3091
 
2903
3092
  const captureExplanations = !(opts && opts.captureExplanations === false);
3093
+ const collectDerived = !(opts && opts.collectDerived === false);
3094
+ const hasDerivedCallback = typeof onDerived === 'function';
2904
3095
  const derivedForward = [];
2905
3096
  const varGen = [0];
2906
3097
  const skCounter = [0];
@@ -3079,8 +3270,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3079
3270
  if (!hasFactIndexed(facts, instantiated)) {
3080
3271
  pushFactIndexed(facts, instantiated);
3081
3272
  const df = makeDerivedRecord(instantiated, r, getInstantiatedPremises(), s, captureExplanations);
3082
- derivedForward.push(df);
3083
- if (typeof onDerived === 'function') onDerived(df);
3273
+ if (collectDerived) derivedForward.push(df);
3274
+ if (hasDerivedCallback) onDerived(df);
3084
3275
  changedHere = true;
3085
3276
  }
3086
3277
 
@@ -3143,8 +3334,8 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3143
3334
 
3144
3335
  pushFactIndexed(facts, inst);
3145
3336
  const df = makeDerivedRecord(inst, r, getInstantiatedPremises(), s, captureExplanations);
3146
- derivedForward.push(df);
3147
- if (typeof onDerived === 'function') onDerived(df);
3337
+ if (collectDerived) derivedForward.push(df);
3338
+ if (hasDerivedCallback) onDerived(df);
3148
3339
 
3149
3340
  changedHere = true;
3150
3341
  }
package/lib/lexer.js CHANGED
@@ -121,15 +121,6 @@ function isIdentChar(c) {
121
121
  return c === ':' || isPnChars(c);
122
122
  }
123
123
 
124
- function canContinueAfterDot(next) {
125
- // PN_LOCAL allows '.' but it cannot appear at the end.
126
- // We include '.' only if it is followed by something that could continue a name.
127
- if (next === null) return false;
128
- if (isIdentChar(next)) return true;
129
- if (next === '%' || next === '\\') return true;
130
- return false;
131
- }
132
-
133
124
  function isForbiddenNoncharacterCodePoint(cp) {
134
125
  return (cp & 0xffff) === 0xfffe || (cp & 0xffff) === 0xffff;
135
126
  }
@@ -1205,7 +1196,9 @@ function lex(inputText, opts = {}) {
1205
1196
  // Avoid copying large ASCII/BMP inputs into an Array. Array.from() is
1206
1197
  // only needed when the text contains surrogate pairs and we want the old
1207
1198
  // code-point iteration behavior for non-BMP characters.
1208
- const chars = /[\uD800-\uDFFF]/.test(inputText) ? Array.from(inputText) : inputText;
1199
+ const hasSurrogates = /[\uD800-\uDFFF]/.test(inputText);
1200
+ const inputMayContainInvalidStringChar = hasSurrogates || /[\u0000\uFFFE\uFFFF]/.test(inputText);
1201
+ const chars = hasSurrogates ? Array.from(inputText) : inputText;
1209
1202
  const n = chars.length;
1210
1203
  let i = 0;
1211
1204
  const tokens = [];
@@ -1240,14 +1233,47 @@ function lex(inputText, opts = {}) {
1240
1233
  // Hard stops: delimiters cannot appear unescaped inside PNAME tokens.
1241
1234
  if (cc === '{' || cc === '}' || cc === '(' || cc === ')' || cc === '[' || cc === ']' || cc === ';' || cc === ',') break;
1242
1235
 
1243
- // Dot is allowed inside PN_LOCAL, but not at the end.
1244
- if (cc === '.') {
1245
- if (!canContinueAfterDot(peek(1))) break;
1246
- if (out !== null) out.push('.');
1236
+ const code = cc.charCodeAt(0);
1237
+
1238
+ // Common ASCII QName/identifier characters. Keep this branch inline so
1239
+ // ordinary N3 files do not call through the full Unicode PN_CHARS predicate
1240
+ // for every character.
1241
+ if (
1242
+ code === 58 || // ':'
1243
+ code === 95 || // '_'
1244
+ code === 45 || // '-'
1245
+ (code >= 48 && code <= 57) ||
1246
+ (code >= 65 && code <= 90) ||
1247
+ (code >= 97 && code <= 122)
1248
+ ) {
1249
+ if (out !== null) out.push(cc);
1247
1250
  i++;
1248
1251
  continue;
1249
1252
  }
1250
1253
 
1254
+ // Dot is allowed inside PN_LOCAL, but not at the end.
1255
+ if (cc === '.') {
1256
+ const next = peek(1);
1257
+ if (next === null) break;
1258
+ const ncode = next.charCodeAt(0);
1259
+ if (
1260
+ next === '%' ||
1261
+ next === '\\' ||
1262
+ ncode === 58 ||
1263
+ ncode === 95 ||
1264
+ ncode === 45 ||
1265
+ (ncode >= 48 && ncode <= 57) ||
1266
+ (ncode >= 65 && ncode <= 90) ||
1267
+ (ncode >= 97 && ncode <= 122) ||
1268
+ isIdentChar(next)
1269
+ ) {
1270
+ if (out !== null) out.push('.');
1271
+ i++;
1272
+ continue;
1273
+ }
1274
+ break;
1275
+ }
1276
+
1251
1277
  // Percent escape: %HH
1252
1278
  if (cc === '%') {
1253
1279
  const h1 = peek(1);
@@ -1495,7 +1521,7 @@ function lex(inputText, opts = {}) {
1495
1521
  }
1496
1522
  const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
1497
1523
  const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
1498
- assertValidStringLiteralValue(decoded, start);
1524
+ if (sChars !== null || inputMayContainInvalidStringChar) assertValidStringLiteralValue(decoded, start);
1499
1525
  const s = JSON.stringify(decoded); // canonical short quoted form
1500
1526
  tokens.push(new Token('Literal', s, start));
1501
1527
  continue;
@@ -1585,7 +1611,7 @@ function lex(inputText, opts = {}) {
1585
1611
  }
1586
1612
  const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
1587
1613
  const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
1588
- assertValidStringLiteralValue(decoded, start);
1614
+ if (sChars !== null || inputMayContainInvalidStringChar) assertValidStringLiteralValue(decoded, start);
1589
1615
  const s = JSON.stringify(decoded); // canonical short quoted form
1590
1616
  tokens.push(new Token('Literal', s, start));
1591
1617
  continue;