eyeprolog 1.6.38 → 1.6.39

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.6.38",
6
+ "version": "1.6.39",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -212,6 +212,7 @@ export function readProofDocument(text, program, fail = () => {}) {
212
212
  doubleQuotes: program?.doubleQuotes ?? 'chars',
213
213
  operatorDefinitions: [...(program?.operators?.values() ?? [])],
214
214
  sourceMetadata: false,
215
+ generalParser: true,
215
216
  });
216
217
  } catch (error) {
217
218
  fail('C3', `the document does not read: ${error?.message ?? String(error)}`, null);
package/src/parser.js CHANGED
@@ -51,10 +51,14 @@ function scanFastRangeToken(text, i, end) {
51
51
  return i;
52
52
  }
53
53
 
54
+ // The Unicode property tests below are needed only past ASCII: for a code
55
+ // under 128 the plain code tests give exactly the same answer, and these
56
+ // helpers run once per source character.
54
57
  function isWhitespaceCharacter(character) {
55
58
  if (!character) return false;
56
59
  const code = character.charCodeAt(0);
57
- return isWhitespaceCode(code) || /\p{White_Space}/u.test(character);
60
+ if (code < 128) return isWhitespaceCode(code);
61
+ return /\p{White_Space}/u.test(character);
58
62
  }
59
63
 
60
64
  function isUnicodeUpperCharacter(character) {
@@ -82,21 +86,24 @@ function isNameContinueCode(code) {
82
86
  }
83
87
 
84
88
  function isNameContinueCharacter(character) {
85
- return Boolean(character) && (isNameContinueCode(character.charCodeAt(0)) ||
86
- isUnicodeNameContinueCharacter(character));
89
+ if (!character) return false;
90
+ const code = character.charCodeAt(0);
91
+ if (code < 128) return isNameContinueCode(code);
92
+ return isUnicodeNameContinueCharacter(character);
87
93
  }
88
94
 
89
95
  function isVariableStartCharacter(character) {
90
96
  if (!character) return false;
91
97
  const code = character.charCodeAt(0);
92
- return code === 95 || (code >= 65 && code <= 90) || isUnicodeUpperCharacter(character);
98
+ if (code < 128) return code === 95 || (code >= 65 && code <= 90);
99
+ return isUnicodeUpperCharacter(character);
93
100
  }
94
101
 
95
102
  function isPlainAtomStartCharacter(character) {
96
103
  if (!character) return false;
97
104
  const code = character.charCodeAt(0);
98
- return (code >= 97 && code <= 122) ||
99
- (isUnicodeLetterCharacter(character) && !isUnicodeUpperCharacter(character));
105
+ if (code < 128) return code >= 97 && code <= 122;
106
+ return isUnicodeLetterCharacter(character) && !isUnicodeUpperCharacter(character);
100
107
  }
101
108
 
102
109
  const graphicAtomChars = '#$&*+-./<=>?@^~\\:';
@@ -311,6 +318,11 @@ function isCodePointOutOfRange(code) {
311
318
  return code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff);
312
319
  }
313
320
 
321
+ const PUNCTUATION_TOKENS = new Map([
322
+ ['(', TOK.LPAREN], [')', TOK.RPAREN], ['[', TOK.LBRACKET], [']', TOK.RBRACKET],
323
+ ['{', TOK.LBRACE], ['}', TOK.RBRACE], [',', TOK.COMMA], ['|', TOK.BAR], ['.', TOK.DOT],
324
+ ]);
325
+
314
326
  class Parser {
315
327
  constructor(source, options = {}) {
316
328
  this.source = String(source ?? '');
@@ -368,6 +380,7 @@ class Parser {
368
380
  return ch;
369
381
  }
370
382
  convertedSlice(start, end) {
383
+ if (this.parserFlagState.charConversions.size === 0) return this.source.slice(start, end);
371
384
  let out = '';
372
385
  for (const ch of this.source.slice(start, end)) out += this.convertCharacter(ch);
373
386
  return out;
@@ -614,13 +627,10 @@ class Parser {
614
627
  return { type: TOK.ATOM, text: ';', line };
615
628
  }
616
629
 
617
- const punct = {
618
- '(': TOK.LPAREN, ')': TOK.RPAREN, '[': TOK.LBRACKET, ']': TOK.RBRACKET,
619
- '{': TOK.LBRACE, '}': TOK.RBRACE, ',': TOK.COMMA, '|': TOK.BAR, '.': TOK.DOT,
620
- };
621
- if (punct[ch]) {
630
+ const punctuation = PUNCTUATION_TOKENS.get(ch);
631
+ if (punctuation) {
622
632
  this.take();
623
- return { type: punct[ch], text: ch, line, precededByLayout };
633
+ return { type: punctuation, text: ch, line, precededByLayout };
624
634
  }
625
635
  if (ch === ':' && this.peek(1) === '-' &&
626
636
  !(isGraphicAtomCode(this.peek(2).charCodeAt(0)) && !this.terminatingFullStop(this.pos + 2))) {
@@ -671,7 +681,9 @@ class Parser {
671
681
  // A signed numeric literal is only recognized where a term may start.
672
682
  // Otherwise the minus is the standard infix operator, so compact ISO
673
683
  // syntax such as `X-1` must not be read as `X` followed by `-1`.
674
- const previousEndsTerm = this.previousToken && (
684
+ // Whether the previous token ends a term matters only for a minus sign
685
+ // before a digit, so it is decided only then.
686
+ const previousEndsTerm = () => this.previousToken && (
675
687
  [TOK.VAR, TOK.NUMBER, TOK.STRING, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE].includes(this.previousToken.type) ||
676
688
  (this.previousToken.type === TOK.ATOM &&
677
689
  (this.postfixOperators.has(this.previousToken.text) ||
@@ -679,7 +691,7 @@ class Parser {
679
691
  !this.prefixOperators.has(this.previousToken.text))))
680
692
  );
681
693
  if (isDigitCode(ch.charCodeAt(0)) ||
682
- (ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm)) {
694
+ (ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm())) {
683
695
  const start = this.pos;
684
696
  const negative = this.peek() === '-';
685
697
  if (negative) this.take();
@@ -1435,7 +1447,10 @@ export function parseClauses(source, options = {}) {
1435
1447
  const parserOptions = ownsParserFlagState
1436
1448
  ? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes, charConversion: 'on', charConversions: new Map() } }
1437
1449
  : options;
1438
- if (options.sourceMetadata === false && options.readTermEnd == null) {
1450
+ // `generalParser` reads with the general parser from the start: a text of
1451
+ // large, nested terms, such as a proof document, gains nothing from the
1452
+ // line-oriented fast path and pays for a parser per term in it.
1453
+ if (options.sourceMetadata === false && options.readTermEnd == null && options.generalParser !== true) {
1439
1454
  const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
1440
1455
  if (clauses) return clauses;
1441
1456
  if (ownsParserFlagState) {
@@ -62,10 +62,27 @@ function copyHookResult(term, env, inputNames) {
62
62
  return copy(term);
63
63
  }
64
64
 
65
+ // Whether some clause of a hook could unify with `input` as its first
66
+ // argument. A clause whose first argument has another type, name or arity
67
+ // cannot, so when none can the hook does not run at all: a library such as
68
+ // CLP(Z) expands only its own goals, and asking a fresh solver about every
69
+ // other clause and goal of a program cost more than loading it.
70
+ function hookMayApply(group, input) {
71
+ if (input?.type === VAR) return true;
72
+ for (const clause of group.clauses) {
73
+ const first = clause.head?.args?.[0];
74
+ if (first == null || first.type === VAR) return true;
75
+ if (first.type !== input.type || first.name !== input.name) continue;
76
+ if (first.type !== COMPOUND || first.args.length === input.args.length) return true;
77
+ }
78
+ return false;
79
+ }
80
+
65
81
  function runHook(program, module, name, input, loadContext = null) {
66
82
  for (const owner of hookModules(module)) {
67
83
  const group = exactHookGroup(program, owner, name);
68
84
  if (group == null || group.clauses.length === 0) continue;
85
+ if (!hookMayApply(group, input)) continue;
69
86
 
70
87
  const output = variable(`\u0000source-expansion:${++expansionFresh}`);
71
88
  const goal = compound(name, [input, output]);
@@ -1,7 +1,10 @@
1
1
  // A small ordered worker pool for the suites that do the same work once per
2
2
  // file: run every example, prove every example, check every packaged proof.
3
3
  //
4
- // Results are reported in task order however the workers finish, so a parallel
4
+ // Tasks are handed to workers most expensive first when the caller can
5
+ // estimate a task's cost: a suite whose slowest task starts late spends its
6
+ // last seconds running that one task alone. Results are still reported in task
7
+ // order however the workers finish, so a parallel
5
8
  // suite numbers its tests exactly as the sequential one did and a failure still
6
9
  // stops the run at the same test. Each suite compares inside its worker, so
7
10
  // only a name, an elapsed time, an optional failure, and a small plain-data
@@ -17,6 +20,7 @@ export async function runTasksInParallel({
17
20
  maxWorkers,
18
21
  runLocally,
19
22
  onResult,
23
+ cost = null,
20
24
  }) {
21
25
  if (names.length === 0) return;
22
26
  const parallelism = os.availableParallelism?.() ?? os.cpus().length;
@@ -35,6 +39,13 @@ export async function runTasksInParallel({
35
39
  }
36
40
 
37
41
  const completedResults = new Map();
42
+ // Dispatch order: task indexes by descending estimated cost, ties in task
43
+ // order. Reporting still follows the task order itself.
44
+ const dispatch = names.map((_, index) => index);
45
+ if (typeof cost === 'function') {
46
+ const costs = names.map((name) => cost(name));
47
+ dispatch.sort((left, right) => costs[right] - costs[left] || left - right);
48
+ }
38
49
  let nextTask = 0;
39
50
  let nextReport = 0;
40
51
  let completed = 0;
@@ -72,7 +83,8 @@ export async function runTasksInParallel({
72
83
 
73
84
  const assign = (worker) => {
74
85
  if (nextTask >= names.length || settled) return;
75
- worker.postMessage({ ...message, id: nextTask, name: names[nextTask++] });
86
+ const id = dispatch[nextTask++];
87
+ worker.postMessage({ ...message, id, name: names[id] });
76
88
  };
77
89
 
78
90
  for (const worker of workers) {
@@ -50,6 +50,16 @@ const exampleTaskKinds = {
50
50
  };
51
51
 
52
52
 
53
+ // A packaged proof grows with the derivation it records, so its size is a
54
+ // cheap estimate of how long an example takes to run, prove, and check.
55
+ export function exampleCost(name) {
56
+ try {
57
+ return fs.statSync(path.join(expectedProofDir, name)).size;
58
+ } catch {
59
+ return 0;
60
+ }
61
+ }
62
+
53
63
  function runExampleTasks(names, kind, reporter) {
54
64
  return runTasksInParallel({
55
65
  names,
@@ -57,6 +67,7 @@ function runExampleTasks(names, kind, reporter) {
57
67
  workerData: { exampleWorker: true },
58
68
  message: { kind },
59
69
  maxWorkers: 3,
70
+ cost: exampleCost,
60
71
  runLocally: (name) => exampleTaskKinds[kind](name),
61
72
  onResult: (name, result) => reporter.testResult(name, result),
62
73
  });
@@ -15,7 +15,7 @@ import { isMainThread, parentPort, workerData } from 'node:worker_threads';
15
15
  import { Program } from '../src/index.js';
16
16
  import { checkProofDocument, checkReportTerms, sourceGoals, verdict } from '../src/check-proof.js';
17
17
  import { TestReporter, assertEqual, isMainModule, runStandalone } from './test-style.mjs';
18
- import { proofExamples } from './run-examples.mjs';
18
+ import { exampleCost, proofExamples } from './run-examples.mjs';
19
19
  import { runTasksInParallel, serveTasks } from './parallel-tasks.mjs';
20
20
 
21
21
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -37,6 +37,7 @@ export async function runProofChecking(reporter = new TestReporter()) {
37
37
  workerUrl: new URL(import.meta.url),
38
38
  workerData: { proofCheckingWorker: true },
39
39
  maxWorkers: 3,
40
+ cost: exampleCost,
40
41
  runLocally: (name) => checkPackagedProof(name),
41
42
  onResult: (name, result) => {
42
43
  for (const field of Object.keys(totals)) totals[field] += result.value?.[field] ?? 0;