eyeling 1.24.33 → 1.25.1

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/eyeling.js CHANGED
@@ -4825,7 +4825,7 @@ function main() {
4825
4825
  parseN3Text(text, {
4826
4826
  baseIri: __sourceLabelToBaseIri(sourceLabel),
4827
4827
  label: sourceLabel,
4828
- collectUsedPrefixes: true,
4828
+ collectUsedPrefixes: streamMode,
4829
4829
  keepSourceArtifacts: false,
4830
4830
  rdf: rdfMode,
4831
4831
  }),
@@ -5517,6 +5517,10 @@ const {
5517
5517
  copyQuotedGraphMetadata,
5518
5518
  } = require('./prelude');
5519
5519
 
5520
+ // Inference fuses use sysexits.h EX_DATAERR (65): input/rules made a
5521
+ // forbidden condition provable, rather than a generic usage/runtime error.
5522
+ const INFERENCE_FUSE_EXIT_CODE = 65;
5523
+
5520
5524
  // In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
5521
5525
  // Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
5522
5526
  const RDF_NIL_IRI = RDF_NS + 'nil';
@@ -6532,11 +6536,13 @@ function termFastKey(t) {
6532
6536
  if (t instanceof Iri || t instanceof Blank) return t.__tid;
6533
6537
 
6534
6538
  if (t instanceof Literal) {
6535
- // Very large literals intentionally skip global interning in prelude.js to
6536
- // avoid retaining huge strings forever. Their per-object __tid is therefore
6537
- // not value-stable, so using it here breaks duplicate detection for facts
6538
- // such as long log:outputString blocks that are re-derived during forward
6539
- // chaining. Fall back to a value-based key in that case.
6539
+ // Literal construction already computed a value-stable __tid for ordinary
6540
+ // short literals. Avoid re-running literalParts()/datatype normalization
6541
+ // while building fact indexes; on data-heavy inputs this is a hot path.
6542
+ // Only the rare over-sized literal needs the value-based fallback because
6543
+ // prelude intentionally gives such literals per-object ids to avoid
6544
+ // retaining huge strings in the global interner.
6545
+ if (typeof t.value !== 'string' || t.value.length + 64 <= MAX_LITERAL_TID_LEN) return t.__tid;
6540
6546
  const norm = normalizeLiteralForTid(t.value);
6541
6547
  if (typeof norm === 'string' && norm.length > MAX_LITERAL_TID_LEN) return 'L:' + norm;
6542
6548
  return t.__tid;
@@ -6623,17 +6629,28 @@ function ensureFactIndexes(facts) {
6623
6629
  enumerable: false,
6624
6630
  writable: true,
6625
6631
  });
6632
+ Object.defineProperty(facts, '__keySetComplete', {
6633
+ value: false,
6634
+ enumerable: false,
6635
+ writable: true,
6636
+ });
6626
6637
 
6627
- for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i);
6638
+ // Build lookup indexes eagerly, but do not populate the duplicate-detection
6639
+ // string Set for every input fact. The predicate/subject/object indexes are
6640
+ // enough to verify duplicates when needed; avoiding 100k+ joined string keys
6641
+ // saves substantial time and GC on data-heavy query workloads.
6642
+ for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i, false);
6628
6643
  }
6629
6644
 
6630
- function indexFact(facts, tr, idx) {
6645
+ function indexFact(facts, tr, idx, addKeySet = true) {
6631
6646
  const sk = termFastKey(tr.s);
6632
6647
  const ok = termFastKey(tr.o);
6648
+ let pkForKey = null;
6633
6649
 
6634
6650
  if (tr.p instanceof Iri) {
6635
6651
  // Use predicate term id as the primary key to avoid hashing long IRI strings.
6636
6652
  const pk = tr.p.__tid;
6653
+ pkForKey = pk;
6637
6654
 
6638
6655
  let pb = facts.__byPred.get(pk);
6639
6656
  if (!pb) {
@@ -6691,8 +6708,10 @@ function indexFact(facts, tr, idx) {
6691
6708
  }
6692
6709
  }
6693
6710
 
6694
- const key = tripleFastKey(tr);
6695
- if (key !== null) facts.__keySet.add(key);
6711
+ if (addKeySet && sk !== null && ok !== null) {
6712
+ if (pkForKey === null) pkForKey = termFastKey(tr.p);
6713
+ if (pkForKey !== null) facts.__keySet.add(sk + '\t' + pkForKey + '\t' + ok);
6714
+ }
6696
6715
  }
6697
6716
 
6698
6717
  function candidateFacts(facts, goal) {
@@ -6754,7 +6773,10 @@ function hasFactIndexed(facts, tr) {
6754
6773
  ensureFactIndexes(facts);
6755
6774
 
6756
6775
  const key = tripleFastKey(tr);
6757
- if (key !== null) return facts.__keySet.has(key);
6776
+ if (key !== null) {
6777
+ if (facts.__keySet.has(key)) return true;
6778
+ if (facts.__keySetComplete) return false;
6779
+ }
6758
6780
 
6759
6781
  if (tr.p instanceof Iri) {
6760
6782
  const pk = tr.p.__tid;
@@ -6784,7 +6806,7 @@ function pushFactIndexed(facts, tr) {
6784
6806
  ensureFactIndexes(facts);
6785
6807
  const idx = facts.length;
6786
6808
  facts.push(tr);
6787
- indexFact(facts, tr, idx);
6809
+ indexFact(facts, tr, idx, true);
6788
6810
  }
6789
6811
 
6790
6812
  function makeDerivedRecord(fact, rule, premises, subst, captureExplanations) {
@@ -8321,11 +8343,9 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8321
8343
  const varGen = [0];
8322
8344
  const skCounter = [0];
8323
8345
 
8324
- // Speed up dynamic rule promotion by maintaining O(1) membership sets.
8325
- // (Some workloads derive many rule-producing triples.)
8326
-
8327
- __ensureRuleKeySet(forwardRules);
8328
- __ensureRuleKeySet(backRules);
8346
+ // Rule-key sets are only needed if a program actually derives rule-producing
8347
+ // triples. Building them eagerly is expensive on large static rule sets, so
8348
+ // dynamic-promotion sites create them lazily before duplicate checks.
8329
8349
 
8330
8350
  // Cache head blank-node skolemization per (rule firing, head blank label).
8331
8351
  // This prevents repeatedly generating fresh _:sk_N blanks for the *same*
@@ -8445,7 +8465,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8445
8465
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
8446
8466
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
8447
8467
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
8448
- __exitReasoning(2, 'Inference fuse triggered.');
8468
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
8449
8469
  }
8450
8470
 
8451
8471
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -8504,8 +8524,9 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8504
8524
  newRule.conclusion,
8505
8525
  newRule.__dynamicConclusionTerm || null,
8506
8526
  );
8507
- if (!forwardRules.__ruleKeySet.has(key)) {
8508
- forwardRules.__ruleKeySet.add(key);
8527
+ const forwardRuleKeySet = __ensureRuleKeySet(forwardRules);
8528
+ if (!forwardRuleKeySet.has(key)) {
8529
+ forwardRuleKeySet.add(key);
8509
8530
  forwardRules.push(newRule);
8510
8531
  rulesChanged = true;
8511
8532
  }
@@ -8519,8 +8540,9 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8519
8540
  newRule.conclusion,
8520
8541
  newRule.__dynamicConclusionTerm || null,
8521
8542
  );
8522
- if (!backRules.__ruleKeySet.has(key)) {
8523
- backRules.__ruleKeySet.add(key);
8543
+ const backRuleKeySet = __ensureRuleKeySet(backRules);
8544
+ if (!backRuleKeySet.has(key)) {
8545
+ backRuleKeySet.add(key);
8524
8546
  backRules.push(newRule);
8525
8547
  indexBackRule(backRules, newRule);
8526
8548
  rulesChanged = true;
@@ -8606,7 +8628,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8606
8628
  // Inference fuse
8607
8629
  if (r.isFuse && sols.length) {
8608
8630
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
8609
- __exitReasoning(2, 'Inference fuse triggered.');
8631
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
8610
8632
  }
8611
8633
 
8612
8634
  for (const s of sols) {
@@ -9143,6 +9165,7 @@ module.exports = {
9143
9165
  registerBuiltinModule,
9144
9166
  loadBuiltinModule,
9145
9167
  listBuiltinIris,
9168
+ INFERENCE_FUSE_EXIT_CODE,
9146
9169
  };
9147
9170
 
9148
9171
  };
@@ -9170,6 +9193,7 @@ module.exports = {
9170
9193
  rdfjs: dataFactory,
9171
9194
  main: engine.main,
9172
9195
  version: engine.version,
9196
+ INFERENCE_FUSE_EXIT_CODE: engine.INFERENCE_FUSE_EXIT_CODE,
9173
9197
 
9174
9198
  // internals for playground.html
9175
9199
  lex: engine.lex,
@@ -9456,7 +9480,26 @@ class N3SyntaxError extends SyntaxError {
9456
9480
  }
9457
9481
 
9458
9482
  function isWs(c) {
9459
- return /\s/.test(c);
9483
+ if (c === null || c === undefined) return false;
9484
+ const code = c.charCodeAt(0);
9485
+ // Fast path for the whitespace used by N3/Turtle inputs.
9486
+ return code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d || code === 0x0c;
9487
+ }
9488
+
9489
+ function isAsciiAlphaCode(code) {
9490
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
9491
+ }
9492
+
9493
+ function isAsciiDigitCode(code) {
9494
+ return code >= 48 && code <= 57;
9495
+ }
9496
+
9497
+ function isAsciiAlpha(c) {
9498
+ return c !== null && c !== undefined && isAsciiAlphaCode(c.charCodeAt(0));
9499
+ }
9500
+
9501
+ function isAsciiDigit(c) {
9502
+ return c !== null && c !== undefined && isAsciiDigitCode(c.charCodeAt(0));
9460
9503
  }
9461
9504
 
9462
9505
  // Turtle/N3 prefixed names (PNAME_*) allow many Unicode letters and certain
@@ -9469,13 +9512,18 @@ function isWs(c) {
9469
9512
  //
9470
9513
  // We implement a grammar-aligned matcher for PN_CHARS* and PLX fragments.
9471
9514
  function isHexDigit(c) {
9472
- return c !== null && /^[0-9A-Fa-f]$/.test(c);
9515
+ if (c === null || c === undefined) return false;
9516
+ const code = c.charCodeAt(0);
9517
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102);
9473
9518
  }
9474
9519
 
9475
9520
  function isPnCharsBase(c) {
9476
9521
  // Approximation of PN_CHARS_BASE from the N3 grammar using Unicode properties.
9477
9522
  // Covers most letters used in practice (including ñ) and common scripts.
9478
- return c !== null && /[A-Za-z]|\p{L}|\p{Nl}/u.test(c);
9523
+ if (c === null || c === undefined) return false;
9524
+ const code = c.charCodeAt(0);
9525
+ if (isAsciiAlphaCode(code)) return true;
9526
+ return /\p{L}|\p{Nl}/u.test(c);
9479
9527
  }
9480
9528
 
9481
9529
  function isPnCharsU(c) {
@@ -9485,9 +9533,11 @@ function isPnCharsU(c) {
9485
9533
 
9486
9534
  function isPnChars(c) {
9487
9535
  // PN_CHARS ::= PN_CHARS_U | '-' | [0-9] | U+00B7 | [U+0300-U+036F] | [U+203F-U+2040]
9488
- if (c === null) return false;
9536
+ if (c === null || c === undefined) return false;
9537
+ const code = c.charCodeAt(0);
9538
+ if (isAsciiAlphaCode(code) || isAsciiDigitCode(code) || code === 95 || code === 45) return true;
9489
9539
  if (isPnCharsU(c)) return true;
9490
- if (c === '-' || /[0-9]/.test(c) || c === '\u00B7') return true;
9540
+ if (c === '\u00B7') return true;
9491
9541
  const cp = c.codePointAt(0);
9492
9542
  return (cp >= 0x0300 && cp <= 0x036f) || (cp >= 0x203f && cp <= 0x2040);
9493
9543
  }
@@ -10592,7 +10642,10 @@ function normalizeRdfCompatibility(inputText) {
10592
10642
  function lex(inputText, opts = {}) {
10593
10643
  const rdf = !!(opts && opts.rdf);
10594
10644
  if (rdf) inputText = normalizeRdfCompatibility(inputText);
10595
- const chars = Array.from(inputText);
10645
+ // Avoid copying large ASCII/BMP inputs into an Array. Array.from() is
10646
+ // only needed when the text contains surrogate pairs and we want the old
10647
+ // code-point iteration behavior for non-BMP characters.
10648
+ const chars = /[\uD800-\uDFFF]/.test(inputText) ? Array.from(inputText) : inputText;
10596
10649
  const n = chars.length;
10597
10650
  let i = 0;
10598
10651
  const tokens = [];
@@ -10608,19 +10661,29 @@ function lex(inputText, opts = {}) {
10608
10661
  // - Accepts percent escapes (%HH) as PLX fragments.
10609
10662
  // - Accepts PN_LOCAL_ESC backslash escapes and decodes them ("\\." -> ".").
10610
10663
  // - Accepts '.' inside a name only when it is not terminal.
10664
+ function sliceChars(start, end) {
10665
+ return typeof chars === 'string' ? chars.slice(start, end) : chars.slice(start, end).join('');
10666
+ }
10667
+
10611
10668
  function readIdentText(startOffsetForErrors) {
10612
- const out = [];
10669
+ const start = i;
10670
+ let out = null;
10671
+
10672
+ function appendRawUntilHere() {
10673
+ if (out === null) out = [sliceChars(start, i)];
10674
+ }
10675
+
10613
10676
  while (i < n) {
10614
- const cc = peek();
10615
- if (cc === null || isWs(cc)) break;
10677
+ const cc = chars[i];
10678
+ if (cc === null || cc === undefined || isWs(cc)) break;
10616
10679
 
10617
10680
  // Hard stops: delimiters cannot appear unescaped inside PNAME tokens.
10618
- if ('{}()[];,'.includes(cc)) break;
10681
+ if (cc === '{' || cc === '}' || cc === '(' || cc === ')' || cc === '[' || cc === ']' || cc === ';' || cc === ',') break;
10619
10682
 
10620
10683
  // Dot is allowed inside PN_LOCAL, but not at the end.
10621
10684
  if (cc === '.') {
10622
10685
  if (!canContinueAfterDot(peek(1))) break;
10623
- out.push('.');
10686
+ if (out !== null) out.push('.');
10624
10687
  i++;
10625
10688
  continue;
10626
10689
  }
@@ -10635,6 +10698,7 @@ function lex(inputText, opts = {}) {
10635
10698
  typeof startOffsetForErrors === 'number' ? startOffsetForErrors : i,
10636
10699
  );
10637
10700
  }
10701
+ appendRawUntilHere();
10638
10702
  out.push('%', h1, h2);
10639
10703
  i += 3;
10640
10704
  continue;
@@ -10644,6 +10708,7 @@ function lex(inputText, opts = {}) {
10644
10708
  if (cc === '\\') {
10645
10709
  const esc = peek(1);
10646
10710
  if (esc !== null && PN_LOCAL_ESC_SET.has(esc)) {
10711
+ appendRawUntilHere();
10647
10712
  out.push(esc); // decoded form
10648
10713
  i += 2;
10649
10714
  continue;
@@ -10655,14 +10720,14 @@ function lex(inputText, opts = {}) {
10655
10720
  }
10656
10721
 
10657
10722
  if (isIdentChar(cc)) {
10658
- out.push(cc);
10723
+ if (out !== null) out.push(cc);
10659
10724
  i++;
10660
10725
  continue;
10661
10726
  }
10662
10727
 
10663
10728
  break;
10664
10729
  }
10665
- return out.join('');
10730
+ return out === null ? sliceChars(start, i) : out.join('');
10666
10731
  }
10667
10732
 
10668
10733
  while (i < n) {
@@ -10949,10 +11014,10 @@ function lex(inputText, opts = {}) {
10949
11014
  // "@" [a-zA-Z]+ ("-" [a-zA-Z0-9]+)*
10950
11015
  const tagChars = [];
10951
11016
  let cc = peek();
10952
- if (cc === null || !/[A-Za-z]/.test(cc)) {
11017
+ if (cc === null || !isAsciiAlpha(cc)) {
10953
11018
  throw new N3SyntaxError("Invalid language tag (expected [A-Za-z] after '@')", start);
10954
11019
  }
10955
- while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
11020
+ while ((cc = peek()) !== null && isAsciiAlpha(cc)) {
10956
11021
  tagChars.push(cc);
10957
11022
  i++;
10958
11023
  }
@@ -10976,7 +11041,7 @@ function lex(inputText, opts = {}) {
10976
11041
  // Otherwise, treat as a directive (@prefix, @base)
10977
11042
  const wordChars = [];
10978
11043
  let cc;
10979
- while ((cc = peek()) !== null && /[A-Za-z]/.test(cc)) {
11044
+ while ((cc = peek()) !== null && isAsciiAlpha(cc)) {
10980
11045
  wordChars.push(cc);
10981
11046
  i++;
10982
11047
  }
@@ -10988,19 +11053,19 @@ function lex(inputText, opts = {}) {
10988
11053
  }
10989
11054
 
10990
11055
  // 6) Numeric literal (integer or float)
10991
- if (/[0-9]/.test(c) || (c === '-' && peek(1) !== null && /[0-9]/.test(peek(1)))) {
11056
+ if (isAsciiDigit(c) || (c === '-' && peek(1) !== null && isAsciiDigit(peek(1)))) {
10992
11057
  const start = i;
10993
11058
  const numChars = [c];
10994
11059
  i++;
10995
11060
  while (i < n) {
10996
11061
  const cc = chars[i];
10997
- if (/[0-9]/.test(cc)) {
11062
+ if (isAsciiDigit(cc)) {
10998
11063
  numChars.push(cc);
10999
11064
  i++;
11000
11065
  continue;
11001
11066
  }
11002
11067
  if (cc === '.') {
11003
- if (i + 1 < n && /[0-9]/.test(chars[i + 1])) {
11068
+ if (i + 1 < n && isAsciiDigit(chars[i + 1])) {
11004
11069
  numChars.push('.');
11005
11070
  i++;
11006
11071
  continue;
@@ -11015,14 +11080,14 @@ function lex(inputText, opts = {}) {
11015
11080
  if (i < n && (chars[i] === 'e' || chars[i] === 'E')) {
11016
11081
  let j = i + 1;
11017
11082
  if (j < n && (chars[j] === '+' || chars[j] === '-')) j++;
11018
- if (j < n && /[0-9]/.test(chars[j])) {
11083
+ if (j < n && isAsciiDigit(chars[j])) {
11019
11084
  numChars.push(chars[i]); // e/E
11020
11085
  i++;
11021
11086
  if (i < n && (chars[i] === '+' || chars[i] === '-')) {
11022
11087
  numChars.push(chars[i]);
11023
11088
  i++;
11024
11089
  }
11025
- while (i < n && /[0-9]/.test(chars[i])) {
11090
+ while (i < n && isAsciiDigit(chars[i])) {
11026
11091
  numChars.push(chars[i]);
11027
11092
  i++;
11028
11093
  }
@@ -11471,7 +11536,15 @@ class Parser {
11471
11536
  }
11472
11537
 
11473
11538
  isIdentKeyword(tok, keyword) {
11474
- return tok && tok.typ === 'Ident' && typeof tok.value === 'string' && tok.value.toLowerCase() === keyword;
11539
+ if (!tok || tok.typ !== 'Ident' || typeof tok.value !== 'string') return false;
11540
+ const v = tok.value;
11541
+ if (v.length !== keyword.length) return false;
11542
+ for (let i = 0; i < keyword.length; i++) {
11543
+ const code = v.charCodeAt(i);
11544
+ const lower = code >= 65 && code <= 90 ? code + 32 : code;
11545
+ if (lower !== keyword.charCodeAt(i)) return false;
11546
+ }
11547
+ return true;
11475
11548
  }
11476
11549
 
11477
11550
  canStartSparqlPrefixDirective() {
@@ -12394,21 +12467,40 @@ function literalParts(lit) {
12394
12467
  // equality fast-paths than repeated string key construction.
12395
12468
 
12396
12469
  let __nextTid = 1;
12397
- const __tidIntern = new Map(); // string key -> number
12470
+ const __tidIntern = new Map(); // legacy generic key -> number
12471
+ const __iriTidIntern = new Map(); // IRI value -> number
12472
+ const __blankTidIntern = new Map(); // blank label -> number
12473
+ const __literalTidIntern = new Map(); // normalized literal lexical form -> number
12398
12474
 
12399
12475
  // Avoid storing extremely large literal keys in the global term-id intern map.
12400
12476
  // For huge literals we still assign a unique __tid, but we do not intern the key.
12401
12477
  const MAX_LITERAL_TID_LEN = 1024;
12402
12478
 
12403
- function __getTid(key) {
12404
- let id = __tidIntern.get(key);
12479
+ function __getTidFromMap(map, key) {
12480
+ let id = map.get(key);
12405
12481
  if (!id) {
12406
12482
  id = __nextTid++;
12407
- __tidIntern.set(key, id);
12483
+ map.set(key, id);
12408
12484
  }
12409
12485
  return id;
12410
12486
  }
12411
12487
 
12488
+ function __getTid(key) {
12489
+ return __getTidFromMap(__tidIntern, key);
12490
+ }
12491
+
12492
+ function __getIriTid(value) {
12493
+ return __getTidFromMap(__iriTidIntern, value);
12494
+ }
12495
+
12496
+ function __getBlankTid(label) {
12497
+ return __getTidFromMap(__blankTidIntern, label);
12498
+ }
12499
+
12500
+ function __getLiteralTid(norm) {
12501
+ return __getTidFromMap(__literalTidIntern, norm);
12502
+ }
12503
+
12412
12504
  function __isQuotedLexical(lit) {
12413
12505
  if (typeof lit !== 'string') return false;
12414
12506
  if (lit.length >= 6) {
@@ -12454,6 +12546,14 @@ function __isPlainStringLiteralValue(lit) {
12454
12546
  function normalizeLiteralForTid(lit) {
12455
12547
  // Canonicalize so that plain string and explicit xsd:string share the same id.
12456
12548
  if (typeof lit !== 'string') return lit;
12549
+
12550
+ // Fast path for the overwhelmingly common lexer output for plain string
12551
+ // literals: a canonical JSON-style quoted lexical form with no suffix.
12552
+ // This avoids literalParts()/language-tag parsing for large fact tables.
12553
+ if (lit.length >= 2 && lit.charCodeAt(0) === 34 && lit.charCodeAt(lit.length - 1) === 34 && lit.indexOf('^^') < 0) {
12554
+ return `${lit}^^<${XSD_NS}string>`;
12555
+ }
12556
+
12457
12557
  const [lex, dt] = literalParts(lit);
12458
12558
  if (dt === XSD_NS + 'string') return `${lex}^^<${XSD_NS}string>`;
12459
12559
  if (dt === null && __isPlainStringLiteralValue(lit)) return `${lex}^^<${XSD_NS}string>`;
@@ -12471,7 +12571,7 @@ class Iri extends Term {
12471
12571
  super();
12472
12572
  this.value = value;
12473
12573
  Object.defineProperty(this, '__tid', {
12474
- value: __getTid('I:' + value),
12574
+ value: __getIriTid(value),
12475
12575
  enumerable: false,
12476
12576
  });
12477
12577
  }
@@ -12483,7 +12583,7 @@ class Literal extends Term {
12483
12583
  this.value = value; // raw lexical form, e.g. "foo", 12, true, or "\"1944-08-21\"^^..."
12484
12584
  const norm = normalizeLiteralForTid(value);
12485
12585
  const useIntern = typeof norm === 'string' && norm.length <= MAX_LITERAL_TID_LEN;
12486
- const tid = useIntern ? __getTid('L:' + norm) : __nextTid++;
12586
+ const tid = useIntern ? __getLiteralTid(norm) : __nextTid++;
12487
12587
  Object.defineProperty(this, '__tid', {
12488
12588
  value: tid,
12489
12589
  enumerable: false,
@@ -12503,7 +12603,7 @@ class Blank extends Term {
12503
12603
  super();
12504
12604
  this.label = label; // _:b1, etc.
12505
12605
  Object.defineProperty(this, '__tid', {
12506
- value: __getTid('B:' + label),
12606
+ value: __getBlankTid(label),
12507
12607
  enumerable: false,
12508
12608
  });
12509
12609
  }
package/index.d.ts CHANGED
@@ -209,6 +209,7 @@ declare module 'eyeling' {
209
209
  opts?: Omit<ReasonStreamOptions, 'rdfjs' | 'onDerived'>,
210
210
  ): AsyncIterable<RdfJsQuad>;
211
211
 
212
+ export const INFERENCE_FUSE_EXIT_CODE: 65;
212
213
  export const rdfjs: RdfJsDataFactory;
213
214
  export function registerBuiltin(iri: string, handler: BuiltinHandler): BuiltinHandler;
214
215
  export function unregisterBuiltin(iri: string): boolean;
@@ -237,6 +238,7 @@ declare module 'eyeling/browser' {
237
238
  opts?: Omit<ReasonStreamOptions, 'rdfjs' | 'onDerived'>,
238
239
  ): AsyncIterable<RdfJsQuad>;
239
240
 
241
+ export const INFERENCE_FUSE_EXIT_CODE: 65;
240
242
  export const rdfjs: RdfJsDataFactory;
241
243
  export function registerBuiltin(iri: string, handler: BuiltinHandler): BuiltinHandler;
242
244
  export function unregisterBuiltin(iri: string): boolean;
@@ -247,6 +249,7 @@ declare module 'eyeling/browser' {
247
249
  readonly version: string;
248
250
  reasonStream: typeof reasonStream;
249
251
  reasonRdfJs: typeof reasonRdfJs;
252
+ readonly INFERENCE_FUSE_EXIT_CODE: typeof INFERENCE_FUSE_EXIT_CODE;
250
253
  rdfjs: typeof rdfjs;
251
254
  registerBuiltin: typeof registerBuiltin;
252
255
  unregisterBuiltin: typeof unregisterBuiltin;
package/lib/cli.js CHANGED
@@ -210,7 +210,7 @@ function main() {
210
210
  parseN3Text(text, {
211
211
  baseIri: __sourceLabelToBaseIri(sourceLabel),
212
212
  label: sourceLabel,
213
- collectUsedPrefixes: true,
213
+ collectUsedPrefixes: streamMode,
214
214
  keepSourceArtifacts: false,
215
215
  rdf: rdfMode,
216
216
  }),
package/lib/engine.js CHANGED
@@ -30,6 +30,10 @@ const {
30
30
  copyQuotedGraphMetadata,
31
31
  } = require('./prelude');
32
32
 
33
+ // Inference fuses use sysexits.h EX_DATAERR (65): input/rules made a
34
+ // forbidden condition provable, rather than a generic usage/runtime error.
35
+ const INFERENCE_FUSE_EXIT_CODE = 65;
36
+
33
37
  // In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
34
38
  // Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
35
39
  const RDF_NIL_IRI = RDF_NS + 'nil';
@@ -1045,11 +1049,13 @@ function termFastKey(t) {
1045
1049
  if (t instanceof Iri || t instanceof Blank) return t.__tid;
1046
1050
 
1047
1051
  if (t instanceof Literal) {
1048
- // Very large literals intentionally skip global interning in prelude.js to
1049
- // avoid retaining huge strings forever. Their per-object __tid is therefore
1050
- // not value-stable, so using it here breaks duplicate detection for facts
1051
- // such as long log:outputString blocks that are re-derived during forward
1052
- // chaining. Fall back to a value-based key in that case.
1052
+ // Literal construction already computed a value-stable __tid for ordinary
1053
+ // short literals. Avoid re-running literalParts()/datatype normalization
1054
+ // while building fact indexes; on data-heavy inputs this is a hot path.
1055
+ // Only the rare over-sized literal needs the value-based fallback because
1056
+ // prelude intentionally gives such literals per-object ids to avoid
1057
+ // retaining huge strings in the global interner.
1058
+ if (typeof t.value !== 'string' || t.value.length + 64 <= MAX_LITERAL_TID_LEN) return t.__tid;
1053
1059
  const norm = normalizeLiteralForTid(t.value);
1054
1060
  if (typeof norm === 'string' && norm.length > MAX_LITERAL_TID_LEN) return 'L:' + norm;
1055
1061
  return t.__tid;
@@ -1136,17 +1142,28 @@ function ensureFactIndexes(facts) {
1136
1142
  enumerable: false,
1137
1143
  writable: true,
1138
1144
  });
1145
+ Object.defineProperty(facts, '__keySetComplete', {
1146
+ value: false,
1147
+ enumerable: false,
1148
+ writable: true,
1149
+ });
1139
1150
 
1140
- for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i);
1151
+ // Build lookup indexes eagerly, but do not populate the duplicate-detection
1152
+ // string Set for every input fact. The predicate/subject/object indexes are
1153
+ // enough to verify duplicates when needed; avoiding 100k+ joined string keys
1154
+ // saves substantial time and GC on data-heavy query workloads.
1155
+ for (let i = 0; i < facts.length; i++) indexFact(facts, facts[i], i, false);
1141
1156
  }
1142
1157
 
1143
- function indexFact(facts, tr, idx) {
1158
+ function indexFact(facts, tr, idx, addKeySet = true) {
1144
1159
  const sk = termFastKey(tr.s);
1145
1160
  const ok = termFastKey(tr.o);
1161
+ let pkForKey = null;
1146
1162
 
1147
1163
  if (tr.p instanceof Iri) {
1148
1164
  // Use predicate term id as the primary key to avoid hashing long IRI strings.
1149
1165
  const pk = tr.p.__tid;
1166
+ pkForKey = pk;
1150
1167
 
1151
1168
  let pb = facts.__byPred.get(pk);
1152
1169
  if (!pb) {
@@ -1204,8 +1221,10 @@ function indexFact(facts, tr, idx) {
1204
1221
  }
1205
1222
  }
1206
1223
 
1207
- const key = tripleFastKey(tr);
1208
- if (key !== null) facts.__keySet.add(key);
1224
+ if (addKeySet && sk !== null && ok !== null) {
1225
+ if (pkForKey === null) pkForKey = termFastKey(tr.p);
1226
+ if (pkForKey !== null) facts.__keySet.add(sk + '\t' + pkForKey + '\t' + ok);
1227
+ }
1209
1228
  }
1210
1229
 
1211
1230
  function candidateFacts(facts, goal) {
@@ -1267,7 +1286,10 @@ function hasFactIndexed(facts, tr) {
1267
1286
  ensureFactIndexes(facts);
1268
1287
 
1269
1288
  const key = tripleFastKey(tr);
1270
- if (key !== null) return facts.__keySet.has(key);
1289
+ if (key !== null) {
1290
+ if (facts.__keySet.has(key)) return true;
1291
+ if (facts.__keySetComplete) return false;
1292
+ }
1271
1293
 
1272
1294
  if (tr.p instanceof Iri) {
1273
1295
  const pk = tr.p.__tid;
@@ -1297,7 +1319,7 @@ function pushFactIndexed(facts, tr) {
1297
1319
  ensureFactIndexes(facts);
1298
1320
  const idx = facts.length;
1299
1321
  facts.push(tr);
1300
- indexFact(facts, tr, idx);
1322
+ indexFact(facts, tr, idx, true);
1301
1323
  }
1302
1324
 
1303
1325
  function makeDerivedRecord(fact, rule, premises, subst, captureExplanations) {
@@ -2834,11 +2856,9 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2834
2856
  const varGen = [0];
2835
2857
  const skCounter = [0];
2836
2858
 
2837
- // Speed up dynamic rule promotion by maintaining O(1) membership sets.
2838
- // (Some workloads derive many rule-producing triples.)
2839
-
2840
- __ensureRuleKeySet(forwardRules);
2841
- __ensureRuleKeySet(backRules);
2859
+ // Rule-key sets are only needed if a program actually derives rule-producing
2860
+ // triples. Building them eagerly is expensive on large static rule sets, so
2861
+ // dynamic-promotion sites create them lazily before duplicate checks.
2842
2862
 
2843
2863
  // Cache head blank-node skolemization per (rule firing, head blank label).
2844
2864
  // This prevents repeatedly generating fresh _:sk_N blanks for the *same*
@@ -2958,7 +2978,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2958
2978
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
2959
2979
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
2960
2980
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
2961
- __exitReasoning(2, 'Inference fuse triggered.');
2981
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
2962
2982
  }
2963
2983
 
2964
2984
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -3017,8 +3037,9 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3017
3037
  newRule.conclusion,
3018
3038
  newRule.__dynamicConclusionTerm || null,
3019
3039
  );
3020
- if (!forwardRules.__ruleKeySet.has(key)) {
3021
- forwardRules.__ruleKeySet.add(key);
3040
+ const forwardRuleKeySet = __ensureRuleKeySet(forwardRules);
3041
+ if (!forwardRuleKeySet.has(key)) {
3042
+ forwardRuleKeySet.add(key);
3022
3043
  forwardRules.push(newRule);
3023
3044
  rulesChanged = true;
3024
3045
  }
@@ -3032,8 +3053,9 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3032
3053
  newRule.conclusion,
3033
3054
  newRule.__dynamicConclusionTerm || null,
3034
3055
  );
3035
- if (!backRules.__ruleKeySet.has(key)) {
3036
- backRules.__ruleKeySet.add(key);
3056
+ const backRuleKeySet = __ensureRuleKeySet(backRules);
3057
+ if (!backRuleKeySet.has(key)) {
3058
+ backRuleKeySet.add(key);
3037
3059
  backRules.push(newRule);
3038
3060
  indexBackRule(backRules, newRule);
3039
3061
  rulesChanged = true;
@@ -3119,7 +3141,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3119
3141
  // Inference fuse
3120
3142
  if (r.isFuse && sols.length) {
3121
3143
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
3122
- __exitReasoning(2, 'Inference fuse triggered.');
3144
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
3123
3145
  }
3124
3146
 
3125
3147
  for (const s of sols) {
@@ -3656,4 +3678,5 @@ module.exports = {
3656
3678
  registerBuiltinModule,
3657
3679
  loadBuiltinModule,
3658
3680
  listBuiltinIris,
3681
+ INFERENCE_FUSE_EXIT_CODE,
3659
3682
  };