eyeling 1.25.1 → 1.25.2

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.
@@ -5849,6 +5849,14 @@ function __prepareForwardRule(r) {
5849
5849
  configurable: true,
5850
5850
  });
5851
5851
  }
5852
+ if (!hasOwn.call(r, '__needsForwardSkipCheck')) {
5853
+ Object.defineProperty(r, '__needsForwardSkipCheck', {
5854
+ value: !!(r.__headIsStrictGround || (r.__scopedSkipInfo && r.__scopedSkipInfo.needsSnap)),
5855
+ enumerable: false,
5856
+ writable: false,
5857
+ configurable: true,
5858
+ });
5859
+ }
5852
5860
  }
5853
5861
 
5854
5862
  function __graphTriplesOrTrue(term) {
@@ -6167,6 +6175,11 @@ function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firi
6167
6175
  }
6168
6176
 
6169
6177
  function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
6178
+ // Fast path: the common case has no explicit head blanks. Do not allocate a
6179
+ // replacement Triple or compute a firing key when skolemization cannot change
6180
+ // anything. This matters for long single-premise chains such as
6181
+ // deep-taxonomy-100000, where every derived head triple is otherwise copied.
6182
+ if (!headBlankLabels || headBlankLabels.size === 0) return tr;
6170
6183
  return new Triple(
6171
6184
  skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter, firingKey, globalMap),
6172
6185
  skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter, firingKey, globalMap),
@@ -6642,6 +6655,35 @@ function ensureFactIndexes(facts) {
6642
6655
  for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i, false);
6643
6656
  }
6644
6657
 
6658
+ function cloneFactIndexesForSnapshot(src, dest) {
6659
+ ensureFactIndexes(src);
6660
+
6661
+ function cloneArrayMap(map) {
6662
+ const out = new Map();
6663
+ for (const [k, arr] of map) out.set(k, arr.slice());
6664
+ return out;
6665
+ }
6666
+
6667
+ function cloneNestedArrayMap(map) {
6668
+ const out = new Map();
6669
+ for (const [k, inner] of map) {
6670
+ const innerOut = new Map();
6671
+ for (const [k2, arr] of inner) innerOut.set(k2, arr.slice());
6672
+ out.set(k, innerOut);
6673
+ }
6674
+ return out;
6675
+ }
6676
+
6677
+ Object.defineProperty(dest, '__byPred', { value: cloneArrayMap(src.__byPred), enumerable: false, writable: true });
6678
+ Object.defineProperty(dest, '__byPS', { value: cloneNestedArrayMap(src.__byPS), enumerable: false, writable: true });
6679
+ Object.defineProperty(dest, '__byPO', { value: cloneNestedArrayMap(src.__byPO), enumerable: false, writable: true });
6680
+ Object.defineProperty(dest, '__wildPred', { value: src.__wildPred.slice(), enumerable: false, writable: true });
6681
+ Object.defineProperty(dest, '__wildPS', { value: cloneArrayMap(src.__wildPS), enumerable: false, writable: true });
6682
+ Object.defineProperty(dest, '__wildPO', { value: cloneArrayMap(src.__wildPO), enumerable: false, writable: true });
6683
+ Object.defineProperty(dest, '__keySet', { value: new Set(src.__keySet), enumerable: false, writable: true });
6684
+ Object.defineProperty(dest, '__keySetComplete', { value: !!src.__keySetComplete, enumerable: false, writable: true });
6685
+ }
6686
+
6645
6687
  function indexFact(facts, tr, idx, addKeySet = true) {
6646
6688
  const sk = termFastKey(tr.s);
6647
6689
  const ok = termFastKey(tr.o);
@@ -6928,13 +6970,20 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
6928
6970
  if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
6929
6971
 
6930
6972
  const goal = r.premise[0];
6973
+ const goalSKey = termFastKey(goal.s);
6974
+ const goalOKey = termFastKey(goal.o);
6975
+ const fastSubjectVar = goal.p instanceof Iri && goal.s instanceof Var && goalOKey !== null ? goal.s.name : null;
6976
+ const fastObjectVar = goal.p instanceof Iri && goal.o instanceof Var && goalSKey !== null ? goal.o.name : null;
6931
6977
  const entry = {
6932
6978
  rule: r,
6933
6979
  ruleIndex: i,
6934
6980
  goal,
6935
6981
  goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
6936
- goalSKey: termFastKey(goal.s),
6937
- goalOKey: termFastKey(goal.o),
6982
+ goalSKey,
6983
+ goalOKey,
6984
+ needsSkipCheck: !!r.__needsForwardSkipCheck,
6985
+ fastSubjectVar,
6986
+ fastObjectVar,
6938
6987
  };
6939
6988
 
6940
6989
  index.indexed.add(r);
@@ -8395,7 +8444,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8395
8444
 
8396
8445
  function makeScopedSnapshot() {
8397
8446
  const snap = facts.slice();
8398
- ensureFactIndexes(snap);
8447
+ cloneFactIndexesForSnapshot(facts, snap);
8399
8448
  Object.defineProperty(snap, '__scopedSnapshot', {
8400
8449
  value: snap,
8401
8450
  enumerable: false,
@@ -8449,10 +8498,21 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8449
8498
  let changedHere = false;
8450
8499
  let rulesChanged = false;
8451
8500
 
8452
- // IMPORTANT: one skolem map per *rule firing*
8501
+ // IMPORTANT: one skolem map per *rule firing*. Instantiate premise
8502
+ // triples and build the firing key lazily: normal CLI runs do not capture
8503
+ // proof records, and most rules have no explicit head blanks, so the eager
8504
+ // work was pure allocation on large forward chains.
8453
8505
  const skMap = {};
8454
- const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
8455
- const fireKey = __firingKey(ruleIndex, instantiatedPremises);
8506
+ let instantiatedPremises = null;
8507
+ let fireKey = null;
8508
+ function getInstantiatedPremises() {
8509
+ if (instantiatedPremises === null) instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
8510
+ return instantiatedPremises;
8511
+ }
8512
+ function getFireKey() {
8513
+ if (fireKey === null) fireKey = __firingKey(ruleIndex, getInstantiatedPremises());
8514
+ return fireKey;
8515
+ }
8456
8516
 
8457
8517
  // Support "dynamic" rule heads where the consequent is a term that
8458
8518
  // (after substitution) evaluates to a quoted formula.
@@ -8505,7 +8565,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8505
8565
  if (isFwRuleTriple || isBwRuleTriple) {
8506
8566
  if (!hasFactIndexed(facts, instantiated)) {
8507
8567
  pushFactIndexed(facts, instantiated);
8508
- const df = makeDerivedRecord(instantiated, r, instantiatedPremises, s, captureExplanations);
8568
+ const df = makeDerivedRecord(instantiated, r, getInstantiatedPremises(), s, captureExplanations);
8509
8569
  derivedForward.push(df);
8510
8570
  if (typeof onDerived === 'function') onDerived(df);
8511
8571
  changedHere = true;
@@ -8553,20 +8613,23 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8553
8613
  }
8554
8614
 
8555
8615
  // Only skolemize blank nodes that occur explicitly in the rule head
8556
- const inst = skolemizeTripleForHeadBlanks(
8557
- instantiated,
8558
- headBlankLabelsHere,
8559
- skMap,
8560
- skCounter,
8561
- fireKey,
8562
- headSkolemCache,
8563
- );
8616
+ const inst =
8617
+ headBlankLabelsHere && headBlankLabelsHere.size
8618
+ ? skolemizeTripleForHeadBlanks(
8619
+ instantiated,
8620
+ headBlankLabelsHere,
8621
+ skMap,
8622
+ skCounter,
8623
+ getFireKey(),
8624
+ headSkolemCache,
8625
+ )
8626
+ : instantiated;
8564
8627
 
8565
8628
  if (!isGroundTriple(inst)) continue;
8566
8629
  if (hasFactIndexed(facts, inst)) continue;
8567
8630
 
8568
8631
  pushFactIndexed(facts, inst);
8569
- const df = makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations);
8632
+ const df = makeDerivedRecord(inst, r, getInstantiatedPremises(), s, captureExplanations);
8570
8633
  derivedForward.push(df);
8571
8634
  if (typeof onDerived === 'function') onDerived(df);
8572
8635
 
@@ -8593,10 +8656,19 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8593
8656
  for (let ci = 0; ci < total; ci++) {
8594
8657
  const entry = ci < candidates.exactLen ? candidates.exact[ci] : candidates.wild[ci - candidates.exactLen];
8595
8658
  const r = entry.rule;
8596
- if (__skipForwardRuleNow(r)) continue;
8597
-
8598
- const s = unifyTriple(entry.goal, fact, __emptySubst());
8599
- if (s === null) continue;
8659
+ if (entry.needsSkipCheck && __skipForwardRuleNow(r)) continue;
8660
+
8661
+ let s;
8662
+ if (entry.fastSubjectVar !== null) {
8663
+ s = __emptySubst();
8664
+ s[entry.fastSubjectVar] = fact.s;
8665
+ } else if (entry.fastObjectVar !== null) {
8666
+ s = __emptySubst();
8667
+ s[entry.fastObjectVar] = fact.o;
8668
+ } else {
8669
+ s = unifyTriple(entry.goal, fact, __emptySubst());
8670
+ if (s === null) continue;
8671
+ }
8600
8672
 
8601
8673
  const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
8602
8674
  if (outcome.rulesChanged) {
@@ -8613,7 +8685,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8613
8685
  for (let i = 0; i < forwardRules.length; i++) {
8614
8686
  const r = forwardRules[i];
8615
8687
  if (agendaIndex.indexed.has(r)) continue;
8616
- if (__skipForwardRuleNow(r)) continue;
8688
+ if (r.__needsForwardSkipCheck && __skipForwardRuleNow(r)) continue;
8617
8689
 
8618
8690
  const headIsStrictGround = r.__headIsStrictGround;
8619
8691
  const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
@@ -10639,6 +10711,16 @@ function normalizeRdfCompatibility(inputText) {
10639
10711
  return text;
10640
10712
  }
10641
10713
 
10714
+
10715
+ function isNumericLikeIdentifier(word) {
10716
+ if (typeof word !== 'string' || word.length === 0) return false;
10717
+ for (let j = 0; j < word.length; j++) {
10718
+ const code = word.charCodeAt(j);
10719
+ if (!((code >= 48 && code <= 57) || code === 46 || code === 45)) return false;
10720
+ }
10721
+ return true;
10722
+ }
10723
+
10642
10724
  function lex(inputText, opts = {}) {
10643
10725
  const rdf = !!(opts && opts.rdf);
10644
10726
  if (rdf) inputText = normalizeRdfCompatibility(inputText);
@@ -10806,22 +10888,47 @@ function lex(inputText, opts = {}) {
10806
10888
  continue;
10807
10889
  }
10808
10890
 
10809
- // 5) Single-character punctuation
10810
- if ('{}()[];,.'.includes(c)) {
10811
- const mapping = {
10812
- '{': 'LBrace',
10813
- '}': 'RBrace',
10814
- '(': 'LParen',
10815
- ')': 'RParen',
10816
- '[': 'LBracket',
10817
- ']': 'RBracket',
10818
- ';': 'Semicolon',
10819
- ',': 'Comma',
10820
- '.': 'Dot',
10821
- };
10822
- tokens.push(new Token(mapping[c], null, i));
10823
- i++;
10824
- continue;
10891
+ // 5) Single-character punctuation. Use a switch rather than allocating a
10892
+ // mapping object for every punctuation token in large inputs.
10893
+ switch (c) {
10894
+ case '{':
10895
+ tokens.push(new Token('LBrace', null, i));
10896
+ i++;
10897
+ continue;
10898
+ case '}':
10899
+ tokens.push(new Token('RBrace', null, i));
10900
+ i++;
10901
+ continue;
10902
+ case '(':
10903
+ tokens.push(new Token('LParen', null, i));
10904
+ i++;
10905
+ continue;
10906
+ case ')':
10907
+ tokens.push(new Token('RParen', null, i));
10908
+ i++;
10909
+ continue;
10910
+ case '[':
10911
+ tokens.push(new Token('LBracket', null, i));
10912
+ i++;
10913
+ continue;
10914
+ case ']':
10915
+ tokens.push(new Token('RBracket', null, i));
10916
+ i++;
10917
+ continue;
10918
+ case ';':
10919
+ tokens.push(new Token('Semicolon', null, i));
10920
+ i++;
10921
+ continue;
10922
+ case ',':
10923
+ tokens.push(new Token('Comma', null, i));
10924
+ i++;
10925
+ continue;
10926
+ case '.':
10927
+ tokens.push(new Token('Dot', null, i));
10928
+ i++;
10929
+ continue;
10930
+ default:
10931
+ break;
10825
10932
  }
10826
10933
 
10827
10934
  // String literal: short "..." or long """..."""
@@ -10880,26 +10987,36 @@ function lex(inputText, opts = {}) {
10880
10987
  continue;
10881
10988
  }
10882
10989
 
10883
- // Short string literal " ... "
10990
+ // Short string literal " ... ". Most data files contain plain
10991
+ // unescaped labels; keep that path slice-based and avoid building an
10992
+ // intermediate character array + raw quoted string.
10884
10993
  i++; // consume opening "
10885
- const sChars = [];
10994
+ const contentStart = i;
10995
+ let sChars = null;
10996
+ let closed = false;
10886
10997
  while (i < n) {
10887
10998
  const cc = chars[i];
10888
10999
  i++;
10889
11000
  if (cc === '\\') {
11001
+ if (sChars === null) sChars = [sliceChars(contentStart, i - 1)];
10890
11002
  if (i < n) {
10891
11003
  const esc = chars[i];
10892
11004
  i++;
10893
11005
  sChars.push('\\');
10894
11006
  sChars.push(esc);
11007
+ } else {
11008
+ sChars.push('\\');
10895
11009
  }
10896
11010
  continue;
10897
11011
  }
10898
- if (cc === '"') break;
10899
- sChars.push(cc);
11012
+ if (cc === '"') {
11013
+ closed = true;
11014
+ break;
11015
+ }
11016
+ if (sChars !== null) sChars.push(cc);
10900
11017
  }
10901
- const raw = '"' + sChars.join('') + '"';
10902
- const decoded = decodeN3StringEscapes(stripQuotes(raw), start);
11018
+ const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
11019
+ const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
10903
11020
  assertValidStringLiteralValue(decoded, start);
10904
11021
  const s = JSON.stringify(decoded); // canonical short quoted form
10905
11022
  tokens.push(new Token('Literal', s, start));
@@ -10964,24 +11081,32 @@ function lex(inputText, opts = {}) {
10964
11081
 
10965
11082
  // Short string literal ' ... '
10966
11083
  i++; // consume opening '
10967
- const sChars = [];
11084
+ const contentStart = i;
11085
+ let sChars = null;
11086
+ let closed = false;
10968
11087
  while (i < n) {
10969
11088
  const cc = chars[i];
10970
11089
  i++;
10971
11090
  if (cc === '\\') {
11091
+ if (sChars === null) sChars = [sliceChars(contentStart, i - 1)];
10972
11092
  if (i < n) {
10973
11093
  const esc = chars[i];
10974
11094
  i++;
10975
11095
  sChars.push('\\');
10976
11096
  sChars.push(esc);
11097
+ } else {
11098
+ sChars.push('\\');
10977
11099
  }
10978
11100
  continue;
10979
11101
  }
10980
- if (cc === "'") break;
10981
- sChars.push(cc);
11102
+ if (cc === "'") {
11103
+ closed = true;
11104
+ break;
11105
+ }
11106
+ if (sChars !== null) sChars.push(cc);
10982
11107
  }
10983
- const raw = "'" + sChars.join('') + "'";
10984
- const decoded = decodeN3StringEscapes(stripQuotes(raw), start);
11108
+ const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
11109
+ const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
10985
11110
  assertValidStringLiteralValue(decoded, start);
10986
11111
  const s = JSON.stringify(decoded); // canonical short quoted form
10987
11112
  tokens.push(new Token('Literal', s, start));
@@ -11106,7 +11231,7 @@ function lex(inputText, opts = {}) {
11106
11231
  }
11107
11232
  if (word === 'true' || word === 'false') {
11108
11233
  tokens.push(new Token('Literal', word, start));
11109
- } else if ([...word].every((ch) => /[0-9.-]/.test(ch))) {
11234
+ } else if (isNumericLikeIdentifier(word)) {
11110
11235
  tokens.push(new Token('Literal', word, start));
11111
11236
  } else {
11112
11237
  tokens.push(new Token('Ident', word, start));
@@ -11691,7 +11816,7 @@ class Parser {
11691
11816
  } else if (tok2.typ === 'Ident') {
11692
11817
  const qn = tok2.value || '';
11693
11818
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok2, qn);
11694
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok2, '@prefix directive IRI');
11819
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok2, '@prefix directive IRI');
11695
11820
  iri = this.prefixes.expandQName(qn);
11696
11821
  } else {
11697
11822
  this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
@@ -11708,7 +11833,7 @@ class Parser {
11708
11833
  } else if (tok.typ === 'Ident') {
11709
11834
  const qn = tok.value || '';
11710
11835
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok, qn);
11711
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok, '@base directive IRI');
11836
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok, '@base directive IRI');
11712
11837
  iri = this.prefixes.expandQName(qn);
11713
11838
  } else {
11714
11839
  this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
@@ -11737,7 +11862,7 @@ class Parser {
11737
11862
  } else if (tok2.typ === 'Ident') {
11738
11863
  const qn = tok2.value || '';
11739
11864
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok2, qn);
11740
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok2, '@prefix directive IRI');
11865
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok2, '@prefix directive IRI');
11741
11866
  iri = this.prefixes.expandQName(qn);
11742
11867
  } else {
11743
11868
  this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
@@ -11758,7 +11883,7 @@ class Parser {
11758
11883
  } else if (tok.typ === 'Ident') {
11759
11884
  const qn = tok.value || '';
11760
11885
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok, qn);
11761
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok, 'BASE directive IRI');
11886
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok, 'BASE directive IRI');
11762
11887
  iri = this.prefixes.expandQName(qn);
11763
11888
  } else {
11764
11889
  this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
@@ -11805,14 +11930,18 @@ class Parser {
11805
11930
  const name = val || '';
11806
11931
  if (name === 'a') {
11807
11932
  return internIri(RDF_NS + 'type');
11808
- } else if (name.startsWith('_:')) {
11933
+ }
11934
+ const sep = name.indexOf(':');
11935
+ if (sep === 1 && name.charCodeAt(0) === 95) {
11809
11936
  return new Blank(name);
11810
- } else if (name.includes(':')) {
11811
- assertValidQNamePrefix(name.split(':', 1)[0], this.fail.bind(this), tok);
11812
- return internIri(this.prefixes.expandQName(name));
11813
- } else {
11814
- failInvalidKeywordLikeIdent(this.fail.bind(this), tok, name);
11815
11937
  }
11938
+ if (sep >= 0) {
11939
+ const prefixName = name.slice(0, sep);
11940
+ assertValidQNamePrefix(prefixName, this.fail.bind(this), tok);
11941
+ const base = this.prefixes.map[prefixName] || '';
11942
+ return internIri(base ? base + name.slice(sep + 1) : name);
11943
+ }
11944
+ failInvalidKeywordLikeIdent(this.fail.bind(this), tok, name);
11816
11945
  }
11817
11946
 
11818
11947
  if (typ === 'Literal') {
@@ -11843,7 +11972,7 @@ class Parser {
11843
11972
  } else if (dtTok.typ === 'Ident') {
11844
11973
  const qn = dtTok.value || '';
11845
11974
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), dtTok, qn);
11846
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), dtTok, 'datatype prefixed name');
11975
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), dtTok, 'datatype prefixed name');
11847
11976
  dtIri = this.prefixes.expandQName(qn);
11848
11977
  } else {
11849
11978
  this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
package/eyeling.js CHANGED
@@ -5849,6 +5849,14 @@ function __prepareForwardRule(r) {
5849
5849
  configurable: true,
5850
5850
  });
5851
5851
  }
5852
+ if (!hasOwn.call(r, '__needsForwardSkipCheck')) {
5853
+ Object.defineProperty(r, '__needsForwardSkipCheck', {
5854
+ value: !!(r.__headIsStrictGround || (r.__scopedSkipInfo && r.__scopedSkipInfo.needsSnap)),
5855
+ enumerable: false,
5856
+ writable: false,
5857
+ configurable: true,
5858
+ });
5859
+ }
5852
5860
  }
5853
5861
 
5854
5862
  function __graphTriplesOrTrue(term) {
@@ -6167,6 +6175,11 @@ function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firi
6167
6175
  }
6168
6176
 
6169
6177
  function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
6178
+ // Fast path: the common case has no explicit head blanks. Do not allocate a
6179
+ // replacement Triple or compute a firing key when skolemization cannot change
6180
+ // anything. This matters for long single-premise chains such as
6181
+ // deep-taxonomy-100000, where every derived head triple is otherwise copied.
6182
+ if (!headBlankLabels || headBlankLabels.size === 0) return tr;
6170
6183
  return new Triple(
6171
6184
  skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter, firingKey, globalMap),
6172
6185
  skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter, firingKey, globalMap),
@@ -6642,6 +6655,35 @@ function ensureFactIndexes(facts) {
6642
6655
  for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i, false);
6643
6656
  }
6644
6657
 
6658
+ function cloneFactIndexesForSnapshot(src, dest) {
6659
+ ensureFactIndexes(src);
6660
+
6661
+ function cloneArrayMap(map) {
6662
+ const out = new Map();
6663
+ for (const [k, arr] of map) out.set(k, arr.slice());
6664
+ return out;
6665
+ }
6666
+
6667
+ function cloneNestedArrayMap(map) {
6668
+ const out = new Map();
6669
+ for (const [k, inner] of map) {
6670
+ const innerOut = new Map();
6671
+ for (const [k2, arr] of inner) innerOut.set(k2, arr.slice());
6672
+ out.set(k, innerOut);
6673
+ }
6674
+ return out;
6675
+ }
6676
+
6677
+ Object.defineProperty(dest, '__byPred', { value: cloneArrayMap(src.__byPred), enumerable: false, writable: true });
6678
+ Object.defineProperty(dest, '__byPS', { value: cloneNestedArrayMap(src.__byPS), enumerable: false, writable: true });
6679
+ Object.defineProperty(dest, '__byPO', { value: cloneNestedArrayMap(src.__byPO), enumerable: false, writable: true });
6680
+ Object.defineProperty(dest, '__wildPred', { value: src.__wildPred.slice(), enumerable: false, writable: true });
6681
+ Object.defineProperty(dest, '__wildPS', { value: cloneArrayMap(src.__wildPS), enumerable: false, writable: true });
6682
+ Object.defineProperty(dest, '__wildPO', { value: cloneArrayMap(src.__wildPO), enumerable: false, writable: true });
6683
+ Object.defineProperty(dest, '__keySet', { value: new Set(src.__keySet), enumerable: false, writable: true });
6684
+ Object.defineProperty(dest, '__keySetComplete', { value: !!src.__keySetComplete, enumerable: false, writable: true });
6685
+ }
6686
+
6645
6687
  function indexFact(facts, tr, idx, addKeySet = true) {
6646
6688
  const sk = termFastKey(tr.s);
6647
6689
  const ok = termFastKey(tr.o);
@@ -6928,13 +6970,20 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
6928
6970
  if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
6929
6971
 
6930
6972
  const goal = r.premise[0];
6973
+ const goalSKey = termFastKey(goal.s);
6974
+ const goalOKey = termFastKey(goal.o);
6975
+ const fastSubjectVar = goal.p instanceof Iri && goal.s instanceof Var && goalOKey !== null ? goal.s.name : null;
6976
+ const fastObjectVar = goal.p instanceof Iri && goal.o instanceof Var && goalSKey !== null ? goal.o.name : null;
6931
6977
  const entry = {
6932
6978
  rule: r,
6933
6979
  ruleIndex: i,
6934
6980
  goal,
6935
6981
  goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
6936
- goalSKey: termFastKey(goal.s),
6937
- goalOKey: termFastKey(goal.o),
6982
+ goalSKey,
6983
+ goalOKey,
6984
+ needsSkipCheck: !!r.__needsForwardSkipCheck,
6985
+ fastSubjectVar,
6986
+ fastObjectVar,
6938
6987
  };
6939
6988
 
6940
6989
  index.indexed.add(r);
@@ -8395,7 +8444,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8395
8444
 
8396
8445
  function makeScopedSnapshot() {
8397
8446
  const snap = facts.slice();
8398
- ensureFactIndexes(snap);
8447
+ cloneFactIndexesForSnapshot(facts, snap);
8399
8448
  Object.defineProperty(snap, '__scopedSnapshot', {
8400
8449
  value: snap,
8401
8450
  enumerable: false,
@@ -8449,10 +8498,21 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8449
8498
  let changedHere = false;
8450
8499
  let rulesChanged = false;
8451
8500
 
8452
- // IMPORTANT: one skolem map per *rule firing*
8501
+ // IMPORTANT: one skolem map per *rule firing*. Instantiate premise
8502
+ // triples and build the firing key lazily: normal CLI runs do not capture
8503
+ // proof records, and most rules have no explicit head blanks, so the eager
8504
+ // work was pure allocation on large forward chains.
8453
8505
  const skMap = {};
8454
- const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
8455
- const fireKey = __firingKey(ruleIndex, instantiatedPremises);
8506
+ let instantiatedPremises = null;
8507
+ let fireKey = null;
8508
+ function getInstantiatedPremises() {
8509
+ if (instantiatedPremises === null) instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
8510
+ return instantiatedPremises;
8511
+ }
8512
+ function getFireKey() {
8513
+ if (fireKey === null) fireKey = __firingKey(ruleIndex, getInstantiatedPremises());
8514
+ return fireKey;
8515
+ }
8456
8516
 
8457
8517
  // Support "dynamic" rule heads where the consequent is a term that
8458
8518
  // (after substitution) evaluates to a quoted formula.
@@ -8505,7 +8565,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8505
8565
  if (isFwRuleTriple || isBwRuleTriple) {
8506
8566
  if (!hasFactIndexed(facts, instantiated)) {
8507
8567
  pushFactIndexed(facts, instantiated);
8508
- const df = makeDerivedRecord(instantiated, r, instantiatedPremises, s, captureExplanations);
8568
+ const df = makeDerivedRecord(instantiated, r, getInstantiatedPremises(), s, captureExplanations);
8509
8569
  derivedForward.push(df);
8510
8570
  if (typeof onDerived === 'function') onDerived(df);
8511
8571
  changedHere = true;
@@ -8553,20 +8613,23 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8553
8613
  }
8554
8614
 
8555
8615
  // Only skolemize blank nodes that occur explicitly in the rule head
8556
- const inst = skolemizeTripleForHeadBlanks(
8557
- instantiated,
8558
- headBlankLabelsHere,
8559
- skMap,
8560
- skCounter,
8561
- fireKey,
8562
- headSkolemCache,
8563
- );
8616
+ const inst =
8617
+ headBlankLabelsHere && headBlankLabelsHere.size
8618
+ ? skolemizeTripleForHeadBlanks(
8619
+ instantiated,
8620
+ headBlankLabelsHere,
8621
+ skMap,
8622
+ skCounter,
8623
+ getFireKey(),
8624
+ headSkolemCache,
8625
+ )
8626
+ : instantiated;
8564
8627
 
8565
8628
  if (!isGroundTriple(inst)) continue;
8566
8629
  if (hasFactIndexed(facts, inst)) continue;
8567
8630
 
8568
8631
  pushFactIndexed(facts, inst);
8569
- const df = makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations);
8632
+ const df = makeDerivedRecord(inst, r, getInstantiatedPremises(), s, captureExplanations);
8570
8633
  derivedForward.push(df);
8571
8634
  if (typeof onDerived === 'function') onDerived(df);
8572
8635
 
@@ -8593,10 +8656,19 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8593
8656
  for (let ci = 0; ci < total; ci++) {
8594
8657
  const entry = ci < candidates.exactLen ? candidates.exact[ci] : candidates.wild[ci - candidates.exactLen];
8595
8658
  const r = entry.rule;
8596
- if (__skipForwardRuleNow(r)) continue;
8597
-
8598
- const s = unifyTriple(entry.goal, fact, __emptySubst());
8599
- if (s === null) continue;
8659
+ if (entry.needsSkipCheck && __skipForwardRuleNow(r)) continue;
8660
+
8661
+ let s;
8662
+ if (entry.fastSubjectVar !== null) {
8663
+ s = __emptySubst();
8664
+ s[entry.fastSubjectVar] = fact.s;
8665
+ } else if (entry.fastObjectVar !== null) {
8666
+ s = __emptySubst();
8667
+ s[entry.fastObjectVar] = fact.o;
8668
+ } else {
8669
+ s = unifyTriple(entry.goal, fact, __emptySubst());
8670
+ if (s === null) continue;
8671
+ }
8600
8672
 
8601
8673
  const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
8602
8674
  if (outcome.rulesChanged) {
@@ -8613,7 +8685,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8613
8685
  for (let i = 0; i < forwardRules.length; i++) {
8614
8686
  const r = forwardRules[i];
8615
8687
  if (agendaIndex.indexed.has(r)) continue;
8616
- if (__skipForwardRuleNow(r)) continue;
8688
+ if (r.__needsForwardSkipCheck && __skipForwardRuleNow(r)) continue;
8617
8689
 
8618
8690
  const headIsStrictGround = r.__headIsStrictGround;
8619
8691
  const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
@@ -10639,6 +10711,16 @@ function normalizeRdfCompatibility(inputText) {
10639
10711
  return text;
10640
10712
  }
10641
10713
 
10714
+
10715
+ function isNumericLikeIdentifier(word) {
10716
+ if (typeof word !== 'string' || word.length === 0) return false;
10717
+ for (let j = 0; j < word.length; j++) {
10718
+ const code = word.charCodeAt(j);
10719
+ if (!((code >= 48 && code <= 57) || code === 46 || code === 45)) return false;
10720
+ }
10721
+ return true;
10722
+ }
10723
+
10642
10724
  function lex(inputText, opts = {}) {
10643
10725
  const rdf = !!(opts && opts.rdf);
10644
10726
  if (rdf) inputText = normalizeRdfCompatibility(inputText);
@@ -10806,22 +10888,47 @@ function lex(inputText, opts = {}) {
10806
10888
  continue;
10807
10889
  }
10808
10890
 
10809
- // 5) Single-character punctuation
10810
- if ('{}()[];,.'.includes(c)) {
10811
- const mapping = {
10812
- '{': 'LBrace',
10813
- '}': 'RBrace',
10814
- '(': 'LParen',
10815
- ')': 'RParen',
10816
- '[': 'LBracket',
10817
- ']': 'RBracket',
10818
- ';': 'Semicolon',
10819
- ',': 'Comma',
10820
- '.': 'Dot',
10821
- };
10822
- tokens.push(new Token(mapping[c], null, i));
10823
- i++;
10824
- continue;
10891
+ // 5) Single-character punctuation. Use a switch rather than allocating a
10892
+ // mapping object for every punctuation token in large inputs.
10893
+ switch (c) {
10894
+ case '{':
10895
+ tokens.push(new Token('LBrace', null, i));
10896
+ i++;
10897
+ continue;
10898
+ case '}':
10899
+ tokens.push(new Token('RBrace', null, i));
10900
+ i++;
10901
+ continue;
10902
+ case '(':
10903
+ tokens.push(new Token('LParen', null, i));
10904
+ i++;
10905
+ continue;
10906
+ case ')':
10907
+ tokens.push(new Token('RParen', null, i));
10908
+ i++;
10909
+ continue;
10910
+ case '[':
10911
+ tokens.push(new Token('LBracket', null, i));
10912
+ i++;
10913
+ continue;
10914
+ case ']':
10915
+ tokens.push(new Token('RBracket', null, i));
10916
+ i++;
10917
+ continue;
10918
+ case ';':
10919
+ tokens.push(new Token('Semicolon', null, i));
10920
+ i++;
10921
+ continue;
10922
+ case ',':
10923
+ tokens.push(new Token('Comma', null, i));
10924
+ i++;
10925
+ continue;
10926
+ case '.':
10927
+ tokens.push(new Token('Dot', null, i));
10928
+ i++;
10929
+ continue;
10930
+ default:
10931
+ break;
10825
10932
  }
10826
10933
 
10827
10934
  // String literal: short "..." or long """..."""
@@ -10880,26 +10987,36 @@ function lex(inputText, opts = {}) {
10880
10987
  continue;
10881
10988
  }
10882
10989
 
10883
- // Short string literal " ... "
10990
+ // Short string literal " ... ". Most data files contain plain
10991
+ // unescaped labels; keep that path slice-based and avoid building an
10992
+ // intermediate character array + raw quoted string.
10884
10993
  i++; // consume opening "
10885
- const sChars = [];
10994
+ const contentStart = i;
10995
+ let sChars = null;
10996
+ let closed = false;
10886
10997
  while (i < n) {
10887
10998
  const cc = chars[i];
10888
10999
  i++;
10889
11000
  if (cc === '\\') {
11001
+ if (sChars === null) sChars = [sliceChars(contentStart, i - 1)];
10890
11002
  if (i < n) {
10891
11003
  const esc = chars[i];
10892
11004
  i++;
10893
11005
  sChars.push('\\');
10894
11006
  sChars.push(esc);
11007
+ } else {
11008
+ sChars.push('\\');
10895
11009
  }
10896
11010
  continue;
10897
11011
  }
10898
- if (cc === '"') break;
10899
- sChars.push(cc);
11012
+ if (cc === '"') {
11013
+ closed = true;
11014
+ break;
11015
+ }
11016
+ if (sChars !== null) sChars.push(cc);
10900
11017
  }
10901
- const raw = '"' + sChars.join('') + '"';
10902
- const decoded = decodeN3StringEscapes(stripQuotes(raw), start);
11018
+ const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
11019
+ const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
10903
11020
  assertValidStringLiteralValue(decoded, start);
10904
11021
  const s = JSON.stringify(decoded); // canonical short quoted form
10905
11022
  tokens.push(new Token('Literal', s, start));
@@ -10964,24 +11081,32 @@ function lex(inputText, opts = {}) {
10964
11081
 
10965
11082
  // Short string literal ' ... '
10966
11083
  i++; // consume opening '
10967
- const sChars = [];
11084
+ const contentStart = i;
11085
+ let sChars = null;
11086
+ let closed = false;
10968
11087
  while (i < n) {
10969
11088
  const cc = chars[i];
10970
11089
  i++;
10971
11090
  if (cc === '\\') {
11091
+ if (sChars === null) sChars = [sliceChars(contentStart, i - 1)];
10972
11092
  if (i < n) {
10973
11093
  const esc = chars[i];
10974
11094
  i++;
10975
11095
  sChars.push('\\');
10976
11096
  sChars.push(esc);
11097
+ } else {
11098
+ sChars.push('\\');
10977
11099
  }
10978
11100
  continue;
10979
11101
  }
10980
- if (cc === "'") break;
10981
- sChars.push(cc);
11102
+ if (cc === "'") {
11103
+ closed = true;
11104
+ break;
11105
+ }
11106
+ if (sChars !== null) sChars.push(cc);
10982
11107
  }
10983
- const raw = "'" + sChars.join('') + "'";
10984
- const decoded = decodeN3StringEscapes(stripQuotes(raw), start);
11108
+ const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
11109
+ const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
10985
11110
  assertValidStringLiteralValue(decoded, start);
10986
11111
  const s = JSON.stringify(decoded); // canonical short quoted form
10987
11112
  tokens.push(new Token('Literal', s, start));
@@ -11106,7 +11231,7 @@ function lex(inputText, opts = {}) {
11106
11231
  }
11107
11232
  if (word === 'true' || word === 'false') {
11108
11233
  tokens.push(new Token('Literal', word, start));
11109
- } else if ([...word].every((ch) => /[0-9.-]/.test(ch))) {
11234
+ } else if (isNumericLikeIdentifier(word)) {
11110
11235
  tokens.push(new Token('Literal', word, start));
11111
11236
  } else {
11112
11237
  tokens.push(new Token('Ident', word, start));
@@ -11691,7 +11816,7 @@ class Parser {
11691
11816
  } else if (tok2.typ === 'Ident') {
11692
11817
  const qn = tok2.value || '';
11693
11818
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok2, qn);
11694
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok2, '@prefix directive IRI');
11819
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok2, '@prefix directive IRI');
11695
11820
  iri = this.prefixes.expandQName(qn);
11696
11821
  } else {
11697
11822
  this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
@@ -11708,7 +11833,7 @@ class Parser {
11708
11833
  } else if (tok.typ === 'Ident') {
11709
11834
  const qn = tok.value || '';
11710
11835
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok, qn);
11711
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok, '@base directive IRI');
11836
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok, '@base directive IRI');
11712
11837
  iri = this.prefixes.expandQName(qn);
11713
11838
  } else {
11714
11839
  this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
@@ -11737,7 +11862,7 @@ class Parser {
11737
11862
  } else if (tok2.typ === 'Ident') {
11738
11863
  const qn = tok2.value || '';
11739
11864
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok2, qn);
11740
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok2, '@prefix directive IRI');
11865
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok2, '@prefix directive IRI');
11741
11866
  iri = this.prefixes.expandQName(qn);
11742
11867
  } else {
11743
11868
  this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
@@ -11758,7 +11883,7 @@ class Parser {
11758
11883
  } else if (tok.typ === 'Ident') {
11759
11884
  const qn = tok.value || '';
11760
11885
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok, qn);
11761
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok, 'BASE directive IRI');
11886
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok, 'BASE directive IRI');
11762
11887
  iri = this.prefixes.expandQName(qn);
11763
11888
  } else {
11764
11889
  this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
@@ -11805,14 +11930,18 @@ class Parser {
11805
11930
  const name = val || '';
11806
11931
  if (name === 'a') {
11807
11932
  return internIri(RDF_NS + 'type');
11808
- } else if (name.startsWith('_:')) {
11933
+ }
11934
+ const sep = name.indexOf(':');
11935
+ if (sep === 1 && name.charCodeAt(0) === 95) {
11809
11936
  return new Blank(name);
11810
- } else if (name.includes(':')) {
11811
- assertValidQNamePrefix(name.split(':', 1)[0], this.fail.bind(this), tok);
11812
- return internIri(this.prefixes.expandQName(name));
11813
- } else {
11814
- failInvalidKeywordLikeIdent(this.fail.bind(this), tok, name);
11815
11937
  }
11938
+ if (sep >= 0) {
11939
+ const prefixName = name.slice(0, sep);
11940
+ assertValidQNamePrefix(prefixName, this.fail.bind(this), tok);
11941
+ const base = this.prefixes.map[prefixName] || '';
11942
+ return internIri(base ? base + name.slice(sep + 1) : name);
11943
+ }
11944
+ failInvalidKeywordLikeIdent(this.fail.bind(this), tok, name);
11816
11945
  }
11817
11946
 
11818
11947
  if (typ === 'Literal') {
@@ -11843,7 +11972,7 @@ class Parser {
11843
11972
  } else if (dtTok.typ === 'Ident') {
11844
11973
  const qn = dtTok.value || '';
11845
11974
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), dtTok, qn);
11846
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), dtTok, 'datatype prefixed name');
11975
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), dtTok, 'datatype prefixed name');
11847
11976
  dtIri = this.prefixes.expandQName(qn);
11848
11977
  } else {
11849
11978
  this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
package/lib/engine.js CHANGED
@@ -362,6 +362,14 @@ function __prepareForwardRule(r) {
362
362
  configurable: true,
363
363
  });
364
364
  }
365
+ if (!hasOwn.call(r, '__needsForwardSkipCheck')) {
366
+ Object.defineProperty(r, '__needsForwardSkipCheck', {
367
+ value: !!(r.__headIsStrictGround || (r.__scopedSkipInfo && r.__scopedSkipInfo.needsSnap)),
368
+ enumerable: false,
369
+ writable: false,
370
+ configurable: true,
371
+ });
372
+ }
365
373
  }
366
374
 
367
375
  function __graphTriplesOrTrue(term) {
@@ -680,6 +688,11 @@ function skolemizeTermForHeadBlanks(t, headBlankLabels, mapping, skCounter, firi
680
688
  }
681
689
 
682
690
  function skolemizeTripleForHeadBlanks(tr, headBlankLabels, mapping, skCounter, firingKey, globalMap) {
691
+ // Fast path: the common case has no explicit head blanks. Do not allocate a
692
+ // replacement Triple or compute a firing key when skolemization cannot change
693
+ // anything. This matters for long single-premise chains such as
694
+ // deep-taxonomy-100000, where every derived head triple is otherwise copied.
695
+ if (!headBlankLabels || headBlankLabels.size === 0) return tr;
683
696
  return new Triple(
684
697
  skolemizeTermForHeadBlanks(tr.s, headBlankLabels, mapping, skCounter, firingKey, globalMap),
685
698
  skolemizeTermForHeadBlanks(tr.p, headBlankLabels, mapping, skCounter, firingKey, globalMap),
@@ -1155,6 +1168,35 @@ function ensureFactIndexes(facts) {
1155
1168
  for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i, false);
1156
1169
  }
1157
1170
 
1171
+ function cloneFactIndexesForSnapshot(src, dest) {
1172
+ ensureFactIndexes(src);
1173
+
1174
+ function cloneArrayMap(map) {
1175
+ const out = new Map();
1176
+ for (const [k, arr] of map) out.set(k, arr.slice());
1177
+ return out;
1178
+ }
1179
+
1180
+ function cloneNestedArrayMap(map) {
1181
+ const out = new Map();
1182
+ for (const [k, inner] of map) {
1183
+ const innerOut = new Map();
1184
+ for (const [k2, arr] of inner) innerOut.set(k2, arr.slice());
1185
+ out.set(k, innerOut);
1186
+ }
1187
+ return out;
1188
+ }
1189
+
1190
+ Object.defineProperty(dest, '__byPred', { value: cloneArrayMap(src.__byPred), enumerable: false, writable: true });
1191
+ Object.defineProperty(dest, '__byPS', { value: cloneNestedArrayMap(src.__byPS), enumerable: false, writable: true });
1192
+ 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 });
1196
+ Object.defineProperty(dest, '__keySet', { value: new Set(src.__keySet), enumerable: false, writable: true });
1197
+ Object.defineProperty(dest, '__keySetComplete', { value: !!src.__keySetComplete, enumerable: false, writable: true });
1198
+ }
1199
+
1158
1200
  function indexFact(facts, tr, idx, addKeySet = true) {
1159
1201
  const sk = termFastKey(tr.s);
1160
1202
  const ok = termFastKey(tr.o);
@@ -1441,13 +1483,20 @@ function makeSinglePremiseAgendaIndex(forwardRules, backRules) {
1441
1483
  if (!isSinglePremiseAgendaRuleSafe(r, backRules)) continue;
1442
1484
 
1443
1485
  const goal = r.premise[0];
1486
+ const goalSKey = termFastKey(goal.s);
1487
+ const goalOKey = termFastKey(goal.o);
1488
+ const fastSubjectVar = goal.p instanceof Iri && goal.s instanceof Var && goalOKey !== null ? goal.s.name : null;
1489
+ const fastObjectVar = goal.p instanceof Iri && goal.o instanceof Var && goalSKey !== null ? goal.o.name : null;
1444
1490
  const entry = {
1445
1491
  rule: r,
1446
1492
  ruleIndex: i,
1447
1493
  goal,
1448
1494
  goalPredTid: goal.p instanceof Iri ? goal.p.__tid : null,
1449
- goalSKey: termFastKey(goal.s),
1450
- goalOKey: termFastKey(goal.o),
1495
+ goalSKey,
1496
+ goalOKey,
1497
+ needsSkipCheck: !!r.__needsForwardSkipCheck,
1498
+ fastSubjectVar,
1499
+ fastObjectVar,
1451
1500
  };
1452
1501
 
1453
1502
  index.indexed.add(r);
@@ -2908,7 +2957,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2908
2957
 
2909
2958
  function makeScopedSnapshot() {
2910
2959
  const snap = facts.slice();
2911
- ensureFactIndexes(snap);
2960
+ cloneFactIndexesForSnapshot(facts, snap);
2912
2961
  Object.defineProperty(snap, '__scopedSnapshot', {
2913
2962
  value: snap,
2914
2963
  enumerable: false,
@@ -2962,10 +3011,21 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2962
3011
  let changedHere = false;
2963
3012
  let rulesChanged = false;
2964
3013
 
2965
- // IMPORTANT: one skolem map per *rule firing*
3014
+ // IMPORTANT: one skolem map per *rule firing*. Instantiate premise
3015
+ // triples and build the firing key lazily: normal CLI runs do not capture
3016
+ // proof records, and most rules have no explicit head blanks, so the eager
3017
+ // work was pure allocation on large forward chains.
2966
3018
  const skMap = {};
2967
- const instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
2968
- const fireKey = __firingKey(ruleIndex, instantiatedPremises);
3019
+ let instantiatedPremises = null;
3020
+ let fireKey = null;
3021
+ function getInstantiatedPremises() {
3022
+ if (instantiatedPremises === null) instantiatedPremises = r.premise.map((b) => applySubstTriple(b, s));
3023
+ return instantiatedPremises;
3024
+ }
3025
+ function getFireKey() {
3026
+ if (fireKey === null) fireKey = __firingKey(ruleIndex, getInstantiatedPremises());
3027
+ return fireKey;
3028
+ }
2969
3029
 
2970
3030
  // Support "dynamic" rule heads where the consequent is a term that
2971
3031
  // (after substitution) evaluates to a quoted formula.
@@ -3018,7 +3078,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3018
3078
  if (isFwRuleTriple || isBwRuleTriple) {
3019
3079
  if (!hasFactIndexed(facts, instantiated)) {
3020
3080
  pushFactIndexed(facts, instantiated);
3021
- const df = makeDerivedRecord(instantiated, r, instantiatedPremises, s, captureExplanations);
3081
+ const df = makeDerivedRecord(instantiated, r, getInstantiatedPremises(), s, captureExplanations);
3022
3082
  derivedForward.push(df);
3023
3083
  if (typeof onDerived === 'function') onDerived(df);
3024
3084
  changedHere = true;
@@ -3066,20 +3126,23 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3066
3126
  }
3067
3127
 
3068
3128
  // Only skolemize blank nodes that occur explicitly in the rule head
3069
- const inst = skolemizeTripleForHeadBlanks(
3070
- instantiated,
3071
- headBlankLabelsHere,
3072
- skMap,
3073
- skCounter,
3074
- fireKey,
3075
- headSkolemCache,
3076
- );
3129
+ const inst =
3130
+ headBlankLabelsHere && headBlankLabelsHere.size
3131
+ ? skolemizeTripleForHeadBlanks(
3132
+ instantiated,
3133
+ headBlankLabelsHere,
3134
+ skMap,
3135
+ skCounter,
3136
+ getFireKey(),
3137
+ headSkolemCache,
3138
+ )
3139
+ : instantiated;
3077
3140
 
3078
3141
  if (!isGroundTriple(inst)) continue;
3079
3142
  if (hasFactIndexed(facts, inst)) continue;
3080
3143
 
3081
3144
  pushFactIndexed(facts, inst);
3082
- const df = makeDerivedRecord(inst, r, instantiatedPremises, s, captureExplanations);
3145
+ const df = makeDerivedRecord(inst, r, getInstantiatedPremises(), s, captureExplanations);
3083
3146
  derivedForward.push(df);
3084
3147
  if (typeof onDerived === 'function') onDerived(df);
3085
3148
 
@@ -3106,10 +3169,19 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3106
3169
  for (let ci = 0; ci < total; ci++) {
3107
3170
  const entry = ci < candidates.exactLen ? candidates.exact[ci] : candidates.wild[ci - candidates.exactLen];
3108
3171
  const r = entry.rule;
3109
- if (__skipForwardRuleNow(r)) continue;
3110
-
3111
- const s = unifyTriple(entry.goal, fact, __emptySubst());
3112
- if (s === null) continue;
3172
+ if (entry.needsSkipCheck && __skipForwardRuleNow(r)) continue;
3173
+
3174
+ let s;
3175
+ if (entry.fastSubjectVar !== null) {
3176
+ s = __emptySubst();
3177
+ s[entry.fastSubjectVar] = fact.s;
3178
+ } else if (entry.fastObjectVar !== null) {
3179
+ s = __emptySubst();
3180
+ s[entry.fastObjectVar] = fact.o;
3181
+ } else {
3182
+ s = unifyTriple(entry.goal, fact, __emptySubst());
3183
+ if (s === null) continue;
3184
+ }
3113
3185
 
3114
3186
  const outcome = __emitForwardRuleSolution(r, entry.ruleIndex, s);
3115
3187
  if (outcome.rulesChanged) {
@@ -3126,7 +3198,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3126
3198
  for (let i = 0; i < forwardRules.length; i++) {
3127
3199
  const r = forwardRules[i];
3128
3200
  if (agendaIndex.indexed.has(r)) continue;
3129
- if (__skipForwardRuleNow(r)) continue;
3201
+ if (r.__needsForwardSkipCheck && __skipForwardRuleNow(r)) continue;
3130
3202
 
3131
3203
  const headIsStrictGround = r.__headIsStrictGround;
3132
3204
  const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
package/lib/lexer.js CHANGED
@@ -1189,6 +1189,16 @@ function normalizeRdfCompatibility(inputText) {
1189
1189
  return text;
1190
1190
  }
1191
1191
 
1192
+
1193
+ function isNumericLikeIdentifier(word) {
1194
+ if (typeof word !== 'string' || word.length === 0) return false;
1195
+ for (let j = 0; j < word.length; j++) {
1196
+ const code = word.charCodeAt(j);
1197
+ if (!((code >= 48 && code <= 57) || code === 46 || code === 45)) return false;
1198
+ }
1199
+ return true;
1200
+ }
1201
+
1192
1202
  function lex(inputText, opts = {}) {
1193
1203
  const rdf = !!(opts && opts.rdf);
1194
1204
  if (rdf) inputText = normalizeRdfCompatibility(inputText);
@@ -1356,22 +1366,47 @@ function lex(inputText, opts = {}) {
1356
1366
  continue;
1357
1367
  }
1358
1368
 
1359
- // 5) Single-character punctuation
1360
- if ('{}()[];,.'.includes(c)) {
1361
- const mapping = {
1362
- '{': 'LBrace',
1363
- '}': 'RBrace',
1364
- '(': 'LParen',
1365
- ')': 'RParen',
1366
- '[': 'LBracket',
1367
- ']': 'RBracket',
1368
- ';': 'Semicolon',
1369
- ',': 'Comma',
1370
- '.': 'Dot',
1371
- };
1372
- tokens.push(new Token(mapping[c], null, i));
1373
- i++;
1374
- continue;
1369
+ // 5) Single-character punctuation. Use a switch rather than allocating a
1370
+ // mapping object for every punctuation token in large inputs.
1371
+ switch (c) {
1372
+ case '{':
1373
+ tokens.push(new Token('LBrace', null, i));
1374
+ i++;
1375
+ continue;
1376
+ case '}':
1377
+ tokens.push(new Token('RBrace', null, i));
1378
+ i++;
1379
+ continue;
1380
+ case '(':
1381
+ tokens.push(new Token('LParen', null, i));
1382
+ i++;
1383
+ continue;
1384
+ case ')':
1385
+ tokens.push(new Token('RParen', null, i));
1386
+ i++;
1387
+ continue;
1388
+ case '[':
1389
+ tokens.push(new Token('LBracket', null, i));
1390
+ i++;
1391
+ continue;
1392
+ case ']':
1393
+ tokens.push(new Token('RBracket', null, i));
1394
+ i++;
1395
+ continue;
1396
+ case ';':
1397
+ tokens.push(new Token('Semicolon', null, i));
1398
+ i++;
1399
+ continue;
1400
+ case ',':
1401
+ tokens.push(new Token('Comma', null, i));
1402
+ i++;
1403
+ continue;
1404
+ case '.':
1405
+ tokens.push(new Token('Dot', null, i));
1406
+ i++;
1407
+ continue;
1408
+ default:
1409
+ break;
1375
1410
  }
1376
1411
 
1377
1412
  // String literal: short "..." or long """..."""
@@ -1430,26 +1465,36 @@ function lex(inputText, opts = {}) {
1430
1465
  continue;
1431
1466
  }
1432
1467
 
1433
- // Short string literal " ... "
1468
+ // Short string literal " ... ". Most data files contain plain
1469
+ // unescaped labels; keep that path slice-based and avoid building an
1470
+ // intermediate character array + raw quoted string.
1434
1471
  i++; // consume opening "
1435
- const sChars = [];
1472
+ const contentStart = i;
1473
+ let sChars = null;
1474
+ let closed = false;
1436
1475
  while (i < n) {
1437
1476
  const cc = chars[i];
1438
1477
  i++;
1439
1478
  if (cc === '\\') {
1479
+ if (sChars === null) sChars = [sliceChars(contentStart, i - 1)];
1440
1480
  if (i < n) {
1441
1481
  const esc = chars[i];
1442
1482
  i++;
1443
1483
  sChars.push('\\');
1444
1484
  sChars.push(esc);
1485
+ } else {
1486
+ sChars.push('\\');
1445
1487
  }
1446
1488
  continue;
1447
1489
  }
1448
- if (cc === '"') break;
1449
- sChars.push(cc);
1490
+ if (cc === '"') {
1491
+ closed = true;
1492
+ break;
1493
+ }
1494
+ if (sChars !== null) sChars.push(cc);
1450
1495
  }
1451
- const raw = '"' + sChars.join('') + '"';
1452
- const decoded = decodeN3StringEscapes(stripQuotes(raw), start);
1496
+ const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
1497
+ const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
1453
1498
  assertValidStringLiteralValue(decoded, start);
1454
1499
  const s = JSON.stringify(decoded); // canonical short quoted form
1455
1500
  tokens.push(new Token('Literal', s, start));
@@ -1514,24 +1559,32 @@ function lex(inputText, opts = {}) {
1514
1559
 
1515
1560
  // Short string literal ' ... '
1516
1561
  i++; // consume opening '
1517
- const sChars = [];
1562
+ const contentStart = i;
1563
+ let sChars = null;
1564
+ let closed = false;
1518
1565
  while (i < n) {
1519
1566
  const cc = chars[i];
1520
1567
  i++;
1521
1568
  if (cc === '\\') {
1569
+ if (sChars === null) sChars = [sliceChars(contentStart, i - 1)];
1522
1570
  if (i < n) {
1523
1571
  const esc = chars[i];
1524
1572
  i++;
1525
1573
  sChars.push('\\');
1526
1574
  sChars.push(esc);
1575
+ } else {
1576
+ sChars.push('\\');
1527
1577
  }
1528
1578
  continue;
1529
1579
  }
1530
- if (cc === "'") break;
1531
- sChars.push(cc);
1580
+ if (cc === "'") {
1581
+ closed = true;
1582
+ break;
1583
+ }
1584
+ if (sChars !== null) sChars.push(cc);
1532
1585
  }
1533
- const raw = "'" + sChars.join('') + "'";
1534
- const decoded = decodeN3StringEscapes(stripQuotes(raw), start);
1586
+ const rawContent = sChars === null ? sliceChars(contentStart, closed ? i - 1 : i) : sChars.join('');
1587
+ const decoded = sChars === null ? rawContent : decodeN3StringEscapes(rawContent, start);
1535
1588
  assertValidStringLiteralValue(decoded, start);
1536
1589
  const s = JSON.stringify(decoded); // canonical short quoted form
1537
1590
  tokens.push(new Token('Literal', s, start));
@@ -1656,7 +1709,7 @@ function lex(inputText, opts = {}) {
1656
1709
  }
1657
1710
  if (word === 'true' || word === 'false') {
1658
1711
  tokens.push(new Token('Literal', word, start));
1659
- } else if ([...word].every((ch) => /[0-9.-]/.test(ch))) {
1712
+ } else if (isNumericLikeIdentifier(word)) {
1660
1713
  tokens.push(new Token('Literal', word, start));
1661
1714
  } else {
1662
1715
  tokens.push(new Token('Ident', word, start));
package/lib/parser.js CHANGED
@@ -241,7 +241,7 @@ class Parser {
241
241
  } else if (tok2.typ === 'Ident') {
242
242
  const qn = tok2.value || '';
243
243
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok2, qn);
244
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok2, '@prefix directive IRI');
244
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok2, '@prefix directive IRI');
245
245
  iri = this.prefixes.expandQName(qn);
246
246
  } else {
247
247
  this.fail(`Expected IRI after @prefix, got ${tok2.toString()}`, tok2);
@@ -258,7 +258,7 @@ class Parser {
258
258
  } else if (tok.typ === 'Ident') {
259
259
  const qn = tok.value || '';
260
260
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok, qn);
261
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok, '@base directive IRI');
261
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok, '@base directive IRI');
262
262
  iri = this.prefixes.expandQName(qn);
263
263
  } else {
264
264
  this.fail(`Expected IRI after @base, got ${tok.toString()}`, tok);
@@ -287,7 +287,7 @@ class Parser {
287
287
  } else if (tok2.typ === 'Ident') {
288
288
  const qn = tok2.value || '';
289
289
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok2, qn);
290
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok2, '@prefix directive IRI');
290
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok2, '@prefix directive IRI');
291
291
  iri = this.prefixes.expandQName(qn);
292
292
  } else {
293
293
  this.fail(`Expected IRI after PREFIX, got ${tok2.toString()}`, tok2);
@@ -308,7 +308,7 @@ class Parser {
308
308
  } else if (tok.typ === 'Ident') {
309
309
  const qn = tok.value || '';
310
310
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), tok, qn);
311
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), tok, 'BASE directive IRI');
311
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), tok, 'BASE directive IRI');
312
312
  iri = this.prefixes.expandQName(qn);
313
313
  } else {
314
314
  this.fail(`Expected IRI after BASE, got ${tok.toString()}`, tok);
@@ -355,14 +355,18 @@ class Parser {
355
355
  const name = val || '';
356
356
  if (name === 'a') {
357
357
  return internIri(RDF_NS + 'type');
358
- } else if (name.startsWith('_:')) {
358
+ }
359
+ const sep = name.indexOf(':');
360
+ if (sep === 1 && name.charCodeAt(0) === 95) {
359
361
  return new Blank(name);
360
- } else if (name.includes(':')) {
361
- assertValidQNamePrefix(name.split(':', 1)[0], this.fail.bind(this), tok);
362
- return internIri(this.prefixes.expandQName(name));
363
- } else {
364
- failInvalidKeywordLikeIdent(this.fail.bind(this), tok, name);
365
362
  }
363
+ if (sep >= 0) {
364
+ const prefixName = name.slice(0, sep);
365
+ assertValidQNamePrefix(prefixName, this.fail.bind(this), tok);
366
+ const base = this.prefixes.map[prefixName] || '';
367
+ return internIri(base ? base + name.slice(sep + 1) : name);
368
+ }
369
+ failInvalidKeywordLikeIdent(this.fail.bind(this), tok, name);
366
370
  }
367
371
 
368
372
  if (typ === 'Literal') {
@@ -393,7 +397,7 @@ class Parser {
393
397
  } else if (dtTok.typ === 'Ident') {
394
398
  const qn = dtTok.value || '';
395
399
  if (!qn.includes(':')) failInvalidKeywordLikeIdent(this.fail.bind(this), dtTok, qn);
396
- assertValidQNamePrefix(qn.split(':', 1)[0], this.fail.bind(this), dtTok, 'datatype prefixed name');
400
+ assertValidQNamePrefix(qn.slice(0, qn.indexOf(':')), this.fail.bind(this), dtTok, 'datatype prefixed name');
397
401
  dtIri = this.prefixes.expandQName(qn);
398
402
  } else {
399
403
  this.fail(`Expected datatype after ^^, got ${dtTok.toString()}`, dtTok);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.25.1",
3
+ "version": "1.25.2",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [