eyeprolog 1.5.35 → 1.5.36

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.5.35",
6
+ "version": "1.5.36",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/parser.js CHANGED
@@ -238,6 +238,12 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
238
238
  }
239
239
  return state;
240
240
  }
241
+ // Pre-compiled character-class regexes used in the lexer hot path.
242
+ const RE_OCTAL_DIGIT = /^[0-7]$/;
243
+ const RE_HEX_DIGIT = /^[0-9A-Fa-f]$/;
244
+ const RE_DECIMAL_DIGIT = /^[0-9]$/;
245
+ const RE_BINARY_DIGIT = /^[01]$/;
246
+
241
247
 
242
248
  class Parser {
243
249
  constructor(source, options = {}) {
@@ -480,7 +486,7 @@ class Parser {
480
486
 
481
487
  if (escaped === 'x') {
482
488
  let digits = '';
483
- while (/^[0-9A-Fa-f]$/.test(peekChar())) digits += takeChar();
489
+ while (RE_HEX_DIGIT.test(peekChar())) digits += takeChar();
484
490
  if (!digits || takeChar() !== '\\') throw new Error(`parse line ${line}: bad hexadecimal escape`);
485
491
  const code = Number.parseInt(digits, 16);
486
492
  if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
@@ -489,9 +495,9 @@ class Parser {
489
495
  if (this.strictIso && !isStrictIsoPcsCodePoint(code)) throw new CharacterRepresentationError();
490
496
  return String.fromCodePoint(code);
491
497
  }
492
- if (/^[0-7]$/.test(escaped)) {
498
+ if (RE_OCTAL_DIGIT.test(escaped)) {
493
499
  let digits = escaped;
494
- while (/^[0-7]$/.test(peekChar())) digits += takeChar();
500
+ while (RE_OCTAL_DIGIT.test(peekChar())) digits += takeChar();
495
501
  if (takeChar() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
496
502
  const code = Number.parseInt(digits, 8);
497
503
  if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
@@ -503,7 +509,7 @@ class Parser {
503
509
  // A backslash followed by a decimal digit is numeric-escape syntax, but
504
510
  // ISO octal digits are limited to 0..7. Do not reinterpret \8 or \9 as
505
511
  // implementation-specific one-character escapes.
506
- if (/^[0-9]$/.test(escaped)) throw new Error(`parse line ${line}: bad octal escape`);
512
+ if (RE_DECIMAL_DIGIT.test(escaped)) throw new Error(`parse line ${line}: bad octal escape`);
507
513
 
508
514
  // The only remaining ISO meta escapes are the four meta characters from
509
515
  // 6.5.5. Forms such as \c, \d, \e, \u or \. are not quoted
@@ -652,9 +658,9 @@ class Parser {
652
658
  return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
653
659
  }
654
660
  const radixKind = this.peek() === '0' ? this.peek(1) : '';
655
- const radixHasDigit = radixKind === 'b' ? /^[01]$/.test(this.peek(2))
656
- : radixKind === 'o' ? /^[0-7]$/.test(this.peek(2))
657
- : radixKind === 'x' ? /^[0-9A-Fa-f]$/.test(this.peek(2))
661
+ const radixHasDigit = radixKind === 'b' ? RE_BINARY_DIGIT.test(this.peek(2))
662
+ : radixKind === 'o' ? RE_OCTAL_DIGIT.test(this.peek(2))
663
+ : radixKind === 'x' ? RE_HEX_DIGIT.test(this.peek(2))
658
664
  : false;
659
665
  if (radixHasDigit) {
660
666
  this.take();
@@ -1816,14 +1822,14 @@ export function parseNumberTokenText(text, options = {}) {
1816
1822
  value = controls[escaped];
1817
1823
  } else if (escaped === 'x') {
1818
1824
  let digits = '';
1819
- while (/^[0-9A-Fa-f]$/.test(source[position] ?? '')) digits += source[position++];
1825
+ while (RE_HEX_DIGIT.test(source[position] ?? '')) digits += source[position++];
1820
1826
  if (!digits || source[position++] !== '\\') throw invalidNumberTokenError;
1821
1827
  const code = Number.parseInt(digits, 16);
1822
1828
  if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
1823
1829
  value = String.fromCodePoint(code);
1824
- } else if (/^[0-7]$/.test(escaped)) {
1830
+ } else if (RE_OCTAL_DIGIT.test(escaped)) {
1825
1831
  let digits = escaped;
1826
- while (/^[0-7]$/.test(source[position] ?? '')) digits += source[position++];
1832
+ while (RE_OCTAL_DIGIT.test(source[position] ?? '')) digits += source[position++];
1827
1833
  if (source[position++] !== '\\') throw invalidNumberTokenError;
1828
1834
  const code = Number.parseInt(digits, 8);
1829
1835
  if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) throw invalidNumberTokenError;
@@ -65,7 +65,9 @@ function reachableIndexesTransposed(target, deps, candidates) {
65
65
  const reverse = new Map();
66
66
  for (const from of candidates) {
67
67
  if (!reverse.has(from)) reverse.set(from, []);
68
- for (const to of deps[from]) {
68
+ const edges = deps[from];
69
+ if (edges == null) continue;
70
+ for (const to of edges) {
69
71
  if (!candidates.has(to)) continue;
70
72
  let bucket = reverse.get(to);
71
73
  if (bucket == null) { bucket = []; reverse.set(to, bucket); }
package/src/program.js CHANGED
@@ -29,9 +29,12 @@ import {
29
29
  } from './program-indexing.js';
30
30
  export { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program-indexing.js';
31
31
  import {
32
+
32
33
  componentHasNegativeEdge, componentHasCut, reachableIndexes, datalogDependencyClauseCount, isFiniteDatalogGroup, isRangeRestrictedFiniteDatalogGroup, isFiniteWfsDatalogGroup, inferStructuralInputPositions, hasStrictListTailRecursion, hasLinearNumericRecursion, isPiAccumulator, isPortableBetweenGenerator, directGoalDependencyKey, collectGoalDependencies, stronglyConnectedComponents, computeNegationStrata,
33
34
  } from './program-analysis.js';
34
35
 
36
+ const RE_DECIMAL_INT = /^\d+$/;
37
+
35
38
  const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
36
39
  const FAST_PARSE_ABORT = Symbol('fastParseAbort');
37
40
  const PROGRAM_BUILD_BATCH_SIZE = 16384;
@@ -303,7 +306,7 @@ export class Program {
303
306
  const positions = [];
304
307
  for (let index = 0; index < template.args.length; index++) {
305
308
  const spec = template.args[index];
306
- if ((spec.type === 'number' && /^\d+$/.test(spec.name)) ||
309
+ if ((spec.type === 'number' && RE_DECIMAL_INT.test(spec.name)) ||
307
310
  (spec.type === ATOM && spec.name === ':')) positions.push(index);
308
311
  }
309
312
  const definitions = this.moduleMetaPredicates.get(module) ?? new Map();
@@ -1653,7 +1656,7 @@ function moduleExportIndicators(term) {
1653
1656
  if (item.type === COMPOUND && item.name === 'op' && item.arity === 3) {
1654
1657
  const [priority, specifier, names] = item.args;
1655
1658
  const operatorNames = names.type === ATOM ? [names] : properListItems(names, new Env());
1656
- if (priority.type !== NUMBER || !/^\d+$/.test(priority.name) || Number(priority.name) > 1200 ||
1659
+ if (priority.type !== NUMBER || !RE_DECIMAL_INT.test(priority.name) || Number(priority.name) > 1200 ||
1657
1660
  specifier.type !== ATOM || !['fx', 'fy', 'xf', 'yf', 'xfx', 'xfy', 'yfx'].includes(specifier.name) ||
1658
1661
  operatorNames == null || operatorNames.some((name) => name.type !== ATOM)) return null;
1659
1662
  continue;
@@ -1832,7 +1835,7 @@ function staticProcedureModificationError(name, arity) {
1832
1835
 
1833
1836
  function predicateIndicator(name, arity) {
1834
1837
  if (name?.type !== ATOM || arity?.type !== 'number') return null;
1835
- if (!/^\d+$/.test(arity.name)) return null;
1838
+ if (!RE_DECIMAL_INT.test(arity.name)) return null;
1836
1839
  const arityNumber = Number(arity.name);
1837
1840
  return { name: name.name, arity: arityNumber, key: `${name.name}/${arityNumber}` };
1838
1841
  }
package/src/term.js CHANGED
@@ -72,7 +72,7 @@ export class CompactListTerm {
72
72
  mayContainVariable(name, env = null) {
73
73
  if (String(name).startsWith(this._variablePrefix)) {
74
74
  const indexText = String(name).slice(this._variablePrefix.length);
75
- if (/^\d+$/.test(indexText)) {
75
+ if (RE_DIGIT_STR.test(indexText)) {
76
76
  const index = BigInt(indexText);
77
77
  if (index >= this._offset && index < this._offset + this._compactLength) return true;
78
78
  }
@@ -1268,17 +1268,22 @@ export function copyResolved(term, env) {
1268
1268
  }
1269
1269
 
1270
1270
  export function termIsGround(term, env = new Env()) {
1271
+ // Defer the cycle-guard Set until we encounter a compound term, since
1272
+ // the overwhelming majority of ground checks are on acyclic clause data.
1271
1273
  const pending = [term];
1272
- const seen = new Set();
1274
+ let seen = null;
1273
1275
  while (pending.length > 0) {
1274
1276
  const resolved = deref(pending.pop(), env);
1275
1277
  if (resolved.type === VAR) return false;
1278
+ const arity = resolved.args.length;
1279
+ if (arity === 0) continue;
1280
+ if (seen == null) seen = new Set();
1276
1281
  if (seen.has(resolved)) continue;
1277
1282
  seen.add(resolved);
1278
1283
  // Visit leftmost arguments first. Lists and other recursive structures
1279
1284
  // commonly carry their first unbound variable there, allowing a
1280
1285
  // non-ground check to finish without walking the complete tail.
1281
- for (let index = resolved.args.length - 1; index >= 0; index--) {
1286
+ for (let index = arity - 1; index >= 0; index--) {
1282
1287
  pending.push(resolved.args[index]);
1283
1288
  }
1284
1289
  }
@@ -1287,11 +1292,18 @@ export function termIsGround(term, env = new Env()) {
1287
1292
 
1288
1293
  const graphicAtomChars = new Set('!#$&*+-/<=>@^~\\'.split(''));
1289
1294
 
1295
+ const RE_LOWER_IDENT = /^[a-z][A-Za-z0-9_]*$/;
1296
+ const RE_UPPER_IDENT = /^(?:_|[A-Z_][A-Za-z0-9_]*)$/;
1297
+ const RE_LEGACY_VAR = /^\?(?:[A-Za-z_][A-Za-z0-9_]*)?$/;
1298
+ const RE_FLOAT = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
1299
+ const RE_DIGIT_STR = /^\d+$/;
1300
+ const RE_SANITIZE_VAR = /[^A-Za-z0-9_]/g;
1301
+ const RE_UPPER_START = /^[A-Z_]/;
1290
1302
  function atomNeedsQuotes(name) {
1291
1303
  if (!name) return true;
1292
1304
  if (name === '[]' || name === '{}') return false;
1293
1305
  if (name === '\\+' || name === '+' || name === '-' || name === '\\') return true;
1294
- if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
1306
+ if (RE_LOWER_IDENT.test(name)) return false;
1295
1307
  for (const ch of name) if (!graphicAtomChars.has(ch)) return true;
1296
1308
  return false;
1297
1309
  }
@@ -1322,11 +1334,11 @@ function legacyVariableToIso(name) {
1322
1334
 
1323
1335
  function writeVariable(name) {
1324
1336
  name = String(name ?? '');
1325
- if (/^\?(?:[A-Za-z_][A-Za-z0-9_]*)?$/.test(name)) return legacyVariableToIso(name);
1326
- if (/^(?:_|[A-Z_][A-Za-z0-9_]*)$/.test(name)) return name;
1327
- const sanitized = name.replace(/[^A-Za-z0-9_]/g, '_');
1337
+ if (RE_LEGACY_VAR.test(name)) return legacyVariableToIso(name);
1338
+ if (RE_UPPER_IDENT.test(name)) return name;
1339
+ const sanitized = name.replace(RE_SANITIZE_VAR, '_');
1328
1340
  if (!sanitized) return '_';
1329
- return /^[A-Z_]/.test(sanitized) ? sanitized : `_${sanitized}`;
1341
+ return RE_UPPER_START.test(sanitized) ? sanitized : `_${sanitized}`;
1330
1342
  }
1331
1343
 
1332
1344
  function writeString(value, quoteStrings) {
@@ -1363,7 +1375,7 @@ function quotedListSplice(term, env, doubleQuotes) {
1363
1375
  if (item.type !== ATOM || Array.from(item.name).length !== 1) return null;
1364
1376
  characters.push(item.name);
1365
1377
  } else {
1366
- if (item.type !== NUMBER || !/^\d+$/.test(item.name)) return null;
1378
+ if (item.type !== NUMBER || !RE_DIGIT_STR.test(item.name)) return null;
1367
1379
  const code = BigInt(item.name);
1368
1380
  if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
1369
1381
  characters.push(String.fromCodePoint(Number(code)));
@@ -1558,12 +1570,16 @@ function variableRank(name, ranks) {
1558
1570
  return rank;
1559
1571
  }
1560
1572
 
1573
+ // ISO standard order: variables < numbers < atoms < strings < compound.
1574
+ // Defined once to avoid allocating a fresh object literal on every comparison.
1575
+ const TYPE_ORDER = { [VAR]: 0, [NUMBER]: 1, [ATOM]: 2, [STRING]: 3, [COMPOUND]: 4 };
1576
+ const EMPTY_ENV = new Env();
1577
+
1561
1578
  function compareTermsWithRanks(left, right, variableRanks) {
1562
- const rank = (term) => ({ [VAR]: 0, [NUMBER]: 1, [ATOM]: 2, [STRING]: 3, [COMPOUND]: 4 })[term.type];
1563
- left = deref(left, new Env());
1564
- right = deref(right, new Env());
1565
- const lr = rank(left);
1566
- const rr = rank(right);
1579
+ left = deref(left, EMPTY_ENV);
1580
+ right = deref(right, EMPTY_ENV);
1581
+ const lr = TYPE_ORDER[left.type] ?? 0;
1582
+ const rr = TYPE_ORDER[right.type] ?? 0;
1567
1583
  if (lr !== rr) return lr < rr ? -1 : 1;
1568
1584
  if (left.type === NUMBER) {
1569
1585
  const leftInteger = isDecimalInteger(left.name);
@@ -1587,8 +1603,9 @@ function compareTermsWithRanks(left, right, variableRanks) {
1587
1603
  return 0;
1588
1604
  }
1589
1605
 
1606
+ const RE_DECIMAL_INTEGER = /^-?\d+$/;
1590
1607
  export function isDecimalInteger(text) {
1591
- return /^-?\d+$/.test(text ?? '');
1608
+ return RE_DECIMAL_INTEGER.test(text ?? '');
1592
1609
  }
1593
1610
 
1594
1611
  export function compareIntegerText(left, right) {
@@ -1599,7 +1616,7 @@ export function compareIntegerText(left, right) {
1599
1616
 
1600
1617
  export function parseFiniteNumber(text) {
1601
1618
  if (text == null || text === '') return null;
1602
- if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) return null;
1619
+ if (!RE_FLOAT.test(text)) return null;
1603
1620
  const n = Number(text);
1604
1621
  return Number.isFinite(n) ? n : null;
1605
1622
  }
package/src/wfs.js CHANGED
@@ -21,9 +21,15 @@ import {
21
21
  resolvePatternTerm,
22
22
  } from './datalog-common.js';
23
23
 
24
+ const _wfsScalarKeyCache = new WeakMap();
24
25
  function scalarKey(term) {
25
- if (term.type === 'number') return `number\u0000${numberValueKey(term.name)}`;
26
- return `${term.type}\u0000${term.name}`;
26
+ const cached = _wfsScalarKeyCache.get(term);
27
+ if (cached != null) return cached;
28
+ const key = term.type === 'number'
29
+ ? `number\u0000${numberValueKey(term.name)}`
30
+ : `${term.type}\u0000${term.name}`;
31
+ _wfsScalarKeyCache.set(term, key);
32
+ return key;
27
33
  }
28
34
 
29
35
  function sameScalar(left, right) {
package/src/write.js CHANGED
@@ -6,6 +6,13 @@ import {
6
6
 
7
7
  const graphicAtomCharacters = new Set('!#$&*+-./<=>?@^~\\'.split(''));
8
8
  const compactInfixOperators = new Set([':', '..']);
9
+ const RE_LOWER_WORD = /^[a-z][A-Za-z0-9_]*$/;
10
+ const RE_ALNUM_IDENT = /^[A-Za-z0-9_]$/;
11
+ const RE_DIGITS_ONLY = /^\d+$/;
12
+ const RE_LEGACY_VAR_W = /^\?(?:[A-Za-z_][A-Za-z0-9_]*)?$/;
13
+ const RE_UPPER_IDENT_W = /^(?:_|[A-Z_][A-Za-z0-9_]*)$/;
14
+ const RE_SANITIZE_W = /[^A-Za-z0-9_]/g;
15
+ const RE_UPPER_START_W = /^[A-Z_]/;
9
16
 
10
17
  function quotedControlEscape(ch) {
11
18
  if (ch === '\x00') return '\\0\\';
@@ -38,7 +45,7 @@ function atomNeedsQuotes(name) {
38
45
  // be read as a bracketed comment and therefore still requires quoting.
39
46
  if (name === '.') return true;
40
47
  if (name.startsWith('/*')) return true;
41
- if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return false;
48
+ if (RE_LOWER_WORD.test(name)) return false;
42
49
  for (const ch of name) if (!graphicAtomCharacters.has(ch)) return true;
43
50
  return false;
44
51
  }
@@ -80,12 +87,12 @@ function compactBoundaryNeedsSpace(left, right) {
80
87
  // neighbouring identifier/number token. Most predefined word operators
81
88
  // are handled explicitly below; this also protects quoted/custom cases that
82
89
  // render without punctuation.
83
- if (/^[A-Za-z0-9_]$/.test(a) && /^[A-Za-z0-9_]$/.test(b)) return true;
90
+ if (RE_ALNUM_IDENT.test(a) && RE_ALNUM_IDENT.test(b)) return true;
84
91
  return false;
85
92
  }
86
93
 
87
94
  function isWordOperatorToken(token) {
88
- return /^[a-z][A-Za-z0-9_]*$/.test(token);
95
+ return RE_LOWER_WORD.test(token);
89
96
  }
90
97
 
91
98
  function compactPrefixOperator(token, argument) {
@@ -99,7 +106,7 @@ function compactPrefixOperator(token, argument) {
99
106
  }
100
107
 
101
108
  function quotedOperatorAfterNumericNeedsSpace(left, token) {
102
- if (!token.startsWith("'") || !/^[0-9]+$/.test(left)) return false;
109
+ if (!token.startsWith("'") || !RE_DIGITS_ONLY.test(left)) return false;
103
110
  const value = Number(left);
104
111
  // `0'X` starts character-code notation and bases 2..36 start based-number
105
112
  // notation. Base 1 and values above 36 do not, so they need no layout
@@ -135,11 +142,11 @@ function legacyVariableToIso(name) {
135
142
 
136
143
  function writeVariable(name) {
137
144
  name = String(name ?? '');
138
- if (/^\?(?:[A-Za-z_][A-Za-z0-9_]*)?$/.test(name)) return legacyVariableToIso(name);
139
- if (/^(?:_|[A-Z_][A-Za-z0-9_]*)$/.test(name)) return name;
140
- const sanitized = name.replace(/[^A-Za-z0-9_]/g, '_');
145
+ if (RE_LEGACY_VAR_W.test(name)) return legacyVariableToIso(name);
146
+ if (RE_UPPER_IDENT_W.test(name)) return name;
147
+ const sanitized = name.replace(RE_SANITIZE_W, '_');
141
148
  if (!sanitized) return '_';
142
- return /^[A-Z_]/.test(sanitized) ? sanitized : `_${sanitized}`;
149
+ return RE_UPPER_START_W.test(sanitized) ? sanitized : `_${sanitized}`;
143
150
  }
144
151
 
145
152
  function writeString(value) {
@@ -157,7 +164,7 @@ function quotedListCharacter(item, doubleQuotes) {
157
164
  return item.name;
158
165
  }
159
166
  if (doubleQuotes === 'codes') {
160
- if (item.type !== NUMBER || !/^\d+$/.test(item.name)) return null;
167
+ if (item.type !== NUMBER || !RE_DIGITS_ONLY.test(item.name)) return null;
161
168
  const code = BigInt(item.name);
162
169
  if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
163
170
  return String.fromCodePoint(Number(code));
@@ -197,7 +204,7 @@ function operatorName(name) {
197
204
  // output must use the unquoted `|` token (WG17 #181/#290).
198
205
  if (name === '|') return '|';
199
206
  if (name === '.' || name.startsWith('/*')) return quoteAtom(name);
200
- if (/^[a-z][A-Za-z0-9_]*$/.test(name)) return name;
207
+ if (RE_LOWER_WORD.test(name)) return name;
201
208
  if (/^[!#$&*+\-./<=>?@^~\\;:]+$/.test(name)) return name;
202
209
  return quoteAtom(name);
203
210
  }
@@ -334,7 +341,7 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
334
341
 
335
342
  if (options.numbervars && resolved.type === COMPOUND && resolved.name === '$VAR' && resolved.arity === 1) {
336
343
  const index = deref(resolved.args[0], env);
337
- if (index.type === NUMBER && /^\d+$/.test(index.name)) {
344
+ if (index.type === NUMBER && RE_DIGITS_ONLY.test(index.name)) {
338
345
  const name = writeNumberedVariable(Number(index.name));
339
346
  if (name != null) return name;
340
347
  }
@@ -404,7 +411,7 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
404
411
  const childNumbervar = options.numbervars && child.type === COMPOUND &&
405
412
  child.name === '$VAR' && child.arity === 1 && (() => {
406
413
  const index = deref(child.args[0], env);
407
- return index.type === NUMBER && /^\d+$/.test(index.name) &&
414
+ return index.type === NUMBER && RE_DIGITS_ONLY.test(index.name) &&
408
415
  writeNumberedVariable(Number(index.name)) != null;
409
416
  })();
410
417
  const childUsesSpecialNotation = isCons(child) ||