eyeprolog 1.2.41 → 1.2.42

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.2.41",
6
+ "version": "1.2.42",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { goalsFromSource } from './goal-metadata.js';
7
+ import { memoryStatistics } from './platform.js';
7
8
 
8
9
  let engineModule = null;
9
10
  let explanationModule = null;
@@ -230,9 +231,9 @@ async function runDefault(engine, program, options) {
230
231
  } catch (error) {
231
232
  if (error?.name !== 'HaltSignal') throw error;
232
233
  process.exitCode = error.code;
234
+ } finally {
235
+ if (options.stats) printStats(solver.stats);
233
236
  }
234
-
235
- if (options.stats) printStats(solver.stats);
236
237
  }
237
238
 
238
239
  function writeExplanation(explanation, program, resolved, registry) {
@@ -259,7 +260,7 @@ Options:
259
260
  -h, --help Show this help text and exit.
260
261
  -p, --proof Enable proof explanations.
261
262
  -q, --quads Run embedded quad tests and fail if any do not hold.
262
- -s, --stats Print solver statistics to stderr after execution.
263
+ -s, --stats Print solver and memory statistics to stderr after execution.
263
264
  --iso-strict Use ISO/IEC 13211-1 core + Corrigenda 1-3 only;
264
265
  reject EyeProlog language extensions and disable automatic tabling.
265
266
  -v, --version Show the package version and exit.
@@ -294,7 +295,7 @@ function printWarnings(program) {
294
295
 
295
296
  function printStats(stats) {
296
297
  process.stderr.write('eyeprolog stats:\n');
297
- for (const [key, value] of Object.entries(stats)) {
298
+ for (const [key, value] of Object.entries({ ...stats, ...memoryStatistics() })) {
298
299
  process.stderr.write(` ${key}: ${value}\n`);
299
300
  }
300
301
  }
package/src/iso.js CHANGED
@@ -1715,21 +1715,22 @@ function numberListBuiltin(kind) {
1715
1715
  const list = deref(goal.args[1], env);
1716
1716
  if (value.type === VAR && list.type === VAR) throw new PrologError('instantiation_error');
1717
1717
  const text = numberListText(list, env, kind, value.type === NUMBER);
1718
- const next = env.clone();
1719
1718
  if (value.type === NUMBER) {
1720
1719
  if (text != null) {
1721
1720
  const parsed = parseIsoNumber(text);
1722
1721
  if (parsed == null) throw numberSyntaxError;
1723
- if (sameNumber(value, parsed)) yield next;
1722
+ if (sameNumber(value, parsed)) yield env.clone();
1724
1723
  return;
1725
1724
  }
1726
1725
  const items = characters(canonicalNumberText(value)).map((ch) =>
1727
1726
  kind === 'chars' ? atom(ch) : numberTerm(ch.codePointAt(0)));
1727
+ const next = env.clone();
1728
1728
  if (unify(goal.args[1], listFromItems(items), next)) yield next;
1729
1729
  return;
1730
1730
  }
1731
1731
  const parsed = parseIsoNumber(text);
1732
1732
  if (parsed == null) throw numberSyntaxError;
1733
+ const next = env.clone();
1733
1734
  if (unify(goal.args[0], parsed, next)) yield next;
1734
1735
  };
1735
1736
  }
@@ -1979,36 +1980,59 @@ function* phraseBuiltin({ solver, goal, env }) {
1979
1980
  solver.absorbStatsFrom(child);
1980
1981
  }
1981
1982
  }
1982
- export function formalErrorTerm(error) {
1983
- const context = error.contextTerm ?? atom('eyeprolog');
1984
- if (error.formalTerm != null) return compound('error', [error.formalTerm, context]);
1985
- const parse = (text) => {
1986
- const open = text.indexOf('(');
1987
- if (open === -1) return atom(text);
1988
- const name = text.slice(0, open);
1989
- const inner = text.slice(open + 1, -1);
1990
- const args = [];
1991
- let start = 0;
1992
- let depth = 0;
1993
- for (let i = 0; i <= inner.length; i++) {
1994
- const ch = inner[i];
1995
- if (ch === '(') depth++;
1996
- else if (ch === ')') depth--;
1997
- else if ((ch === ',' || i === inner.length) && depth === 0) {
1998
- args.push(parse(inner.slice(start, i).trim()));
1999
- start = i + 1;
2000
- }
1983
+ const defaultErrorContext = atom('eyeprolog');
1984
+
1985
+ function parseFormalErrorTerm(text) {
1986
+ const open = text.indexOf('(');
1987
+ if (open === -1) return atom(text);
1988
+ const name = text.slice(0, open);
1989
+ const inner = text.slice(open + 1, -1);
1990
+ const args = [];
1991
+ let start = 0;
1992
+ let depth = 0;
1993
+ for (let i = 0; i <= inner.length; i++) {
1994
+ const ch = inner[i];
1995
+ if (ch === '(') depth++;
1996
+ else if (ch === ')') depth--;
1997
+ else if ((ch === ',' || i === inner.length) && depth === 0) {
1998
+ args.push(parseFormalErrorTerm(inner.slice(start, i).trim()));
1999
+ start = i + 1;
2001
2000
  }
2002
- return compound(name, args);
2003
- };
2004
- let formal = parse(error.formal);
2001
+ }
2002
+ return compound(name, args);
2003
+ }
2004
+
2005
+ function hasDefaultGroundErrorShape(error) {
2006
+ return error.formalTerm == null && error.culprit == null && error.contextTerm == null;
2007
+ }
2008
+
2009
+ export function formalErrorTerm(error) {
2010
+ // Reused processor errors can occur hundreds of thousands of times in a
2011
+ // caught failure loop. Cache their immutable ground error/2 term on the
2012
+ // error object itself instead of rebuilding the same tree on every catch.
2013
+ if (hasDefaultGroundErrorShape(error) && error._groundErrorTerm != null) {
2014
+ return error._groundErrorTerm;
2015
+ }
2016
+
2017
+ const context = error.contextTerm ?? defaultErrorContext;
2018
+ let formal = error.formalTerm ?? parseFormalErrorTerm(error.formal);
2005
2019
  if (error.culprit != null) {
2006
2020
  if (formal.type === COMPOUND) formal = compound(formal.name, [...formal.args, error.culprit]);
2007
2021
  else if (formal.type === ATOM && formal.name === 'uninstantiation_error') {
2008
2022
  formal = compound(formal.name, [error.culprit]);
2009
2023
  }
2010
2024
  }
2011
- return compound('error', [formal, context]);
2025
+ const term = compound('error', [formal, context]);
2026
+ if (hasDefaultGroundErrorShape(error)) error._groundErrorTerm = term;
2027
+ return term;
2028
+ }
2029
+
2030
+ function prologErrorBall(error) {
2031
+ const term = formalErrorTerm(error);
2032
+ // Ground terms are immutable from unification's perspective: bindings are
2033
+ // recorded only in the catcher Env, so copying them is unnecessary.
2034
+ if (hasDefaultGroundErrorShape(error) || termIsGround(term)) return term;
2035
+ return freshCopy(term, new Env());
2012
2036
  }
2013
2037
  function* catchBuiltin({ solver, goal, env }) {
2014
2038
  const child = solver.cloneForInnerGoal();
@@ -2020,7 +2044,7 @@ function* catchBuiltin({ solver, goal, env }) {
2020
2044
  const ball = error instanceof ThrownTerm
2021
2045
  ? error.term
2022
2046
  : error instanceof PrologError
2023
- ? freshCopy(formalErrorTerm(error), new Env())
2047
+ ? prologErrorBall(error)
2024
2048
  : null;
2025
2049
  if (ball == null) throw error;
2026
2050
  const recovered = env.clone();
package/src/platform.js CHANGED
@@ -23,6 +23,13 @@ export function currentWorkingDirectory() {
23
23
  return isNode && typeof process.cwd === 'function' ? process.cwd() : '/';
24
24
  }
25
25
 
26
+ function oldGenerationUsedSize() {
27
+ if (!isNode || typeof v8?.getHeapSpaceStatistics !== 'function') return null;
28
+ return v8.getHeapSpaceStatistics()
29
+ .filter(({ space_name: name }) => name !== 'read_only_space' && !name.startsWith('new_'))
30
+ .reduce((total, { space_used_size: size }) => total + size, 0);
31
+ }
32
+
26
33
  export function usedHeapSize() {
27
34
  if (isNode && typeof process.memoryUsage === 'function') {
28
35
  // --max-old-space-size constrains V8's old generation, not the complete
@@ -30,11 +37,9 @@ export function usedHeapSize() {
30
37
  // with total heap use makes bursts of collectible new-space objects look
31
38
  // like retained memory. Measure the corresponding non-young spaces when
32
39
  // an old-space limit was supplied.
33
- if (configuredOldSpaceLimit != null && typeof v8?.getHeapSpaceStatistics === 'function') {
34
- const spaces = v8.getHeapSpaceStatistics();
35
- return spaces
36
- .filter(({ space_name: name }) => name !== 'read_only_space' && !name.startsWith('new_'))
37
- .reduce((total, { space_used_size: size }) => total + size, 0);
40
+ if (configuredOldSpaceLimit != null) {
41
+ const oldGeneration = oldGenerationUsedSize();
42
+ if (oldGeneration != null) return oldGeneration;
38
43
  }
39
44
  return process.memoryUsage().heapUsed;
40
45
  }
@@ -42,6 +47,31 @@ export function usedHeapSize() {
42
47
  return Number.isFinite(memory?.usedJSHeapSize) ? memory.usedJSHeapSize : null;
43
48
  }
44
49
 
50
+ export function memoryStatistics() {
51
+ const stats = {};
52
+ if (isNode && typeof process.memoryUsage === 'function') {
53
+ const memory = process.memoryUsage();
54
+ stats.memory_heap_used_bytes = memory.heapUsed;
55
+ const oldGeneration = oldGenerationUsedSize();
56
+ if (oldGeneration != null) stats.memory_old_generation_used_bytes = oldGeneration;
57
+ stats.memory_guard_used_bytes = configuredOldSpaceLimit != null && oldGeneration != null
58
+ ? oldGeneration
59
+ : memory.heapUsed;
60
+ stats.memory_rss_bytes = memory.rss;
61
+ } else {
62
+ const memory = globalThis.performance?.memory;
63
+ if (Number.isFinite(memory?.usedJSHeapSize)) {
64
+ stats.memory_heap_used_bytes = memory.usedJSHeapSize;
65
+ stats.memory_guard_used_bytes = memory.usedJSHeapSize;
66
+ }
67
+ }
68
+ const softLimit = softHeapLimit();
69
+ const hardLimit = hardHeapLimit();
70
+ if (Number.isFinite(softLimit)) stats.memory_soft_limit_bytes = softLimit;
71
+ if (Number.isFinite(hardLimit)) stats.memory_hard_limit_bytes = hardLimit;
72
+ return stats;
73
+ }
74
+
45
75
  export function softHeapLimit() {
46
76
  const limit = hardHeapLimit();
47
77
  // Leave ample room for the generator stack to unwind and for the top level
@@ -39,6 +39,7 @@ import {
39
39
  parseProgramText,
40
40
  } from '../src/index.js';
41
41
  import { ISO_OPERATOR_DEFINITIONS, parseGoalText, parseNumberTokenText } from '../src/parser.js';
42
+ import { PrologError, formalErrorTerm } from '../src/iso.js';
42
43
  import { compareTerms } from '../src/term.js';
43
44
  import { formatTermForWrite } from '../src/write.js';
44
45
  import { selectClauseCandidates } from '../src/program.js';
@@ -1939,13 +1940,29 @@ c4 ?- call((!;1)).
1939
1940
 
1940
1941
 
1941
1942
  {
1942
- name: '--stats prints solver statistics to stderr',
1943
+ name: '--stats prints solver and memory statistics to stderr',
1943
1944
  run: () => {
1944
1945
  const result = runCli(['--stats', '-'], { input: '%% goal: q(X, Y)\np(a, b).\nq(X, Y) :- p(X, Y).\n' });
1945
1946
  assertEqual(result.status, 0, 'exit status');
1946
1947
  assertEqual(result.stdout, 'q(a, b).\n', 'stdout');
1947
1948
  assertIncludes(result.stderr, 'eyeprolog stats:\n', 'stderr');
1948
1949
  assertIncludes(result.stderr, ' solve_goals_calls:', 'stderr');
1950
+ assertIncludes(result.stderr, ' memory_heap_used_bytes:', 'stderr');
1951
+ assertIncludes(result.stderr, ' memory_old_generation_used_bytes:', 'stderr');
1952
+ assertIncludes(result.stderr, ' memory_guard_used_bytes:', 'stderr');
1953
+ assertIncludes(result.stderr, ' memory_rss_bytes:', 'stderr');
1954
+ assertIncludes(result.stderr, ' memory_soft_limit_bytes:', 'stderr');
1955
+ assertIncludes(result.stderr, ' memory_hard_limit_bytes:', 'stderr');
1956
+ },
1957
+ },
1958
+ {
1959
+ name: '--stats is still printed when a query raises an error',
1960
+ run: () => {
1961
+ const result = runCli(['--stats', '-'], { input: "%% goal: number_chars(N, ['x'])\n" });
1962
+ assertEqual(result.status, 1, 'exit status');
1963
+ assertIncludes(result.stderr, 'eyeprolog stats:\n', 'stderr');
1964
+ assertIncludes(result.stderr, ' memory_heap_used_bytes:', 'stderr');
1965
+ assertIncludes(result.stderr, 'eyeprolog: error(syntax_error(number))', 'stderr');
1949
1966
  },
1950
1967
  },
1951
1968
  {
@@ -2959,6 +2976,15 @@ open(X) :- candidate(X), \\+ closed(X).
2959
2976
  assertEqual(result.stdout, '300000', 'fresh-variable answer count');
2960
2977
  },
2961
2978
  },
2979
+ {
2980
+ name: 'ground Prolog error terms are reused across catches',
2981
+ run: () => {
2982
+ const error = new PrologError('syntax_error(number)');
2983
+ const first = formalErrorTerm(error);
2984
+ const second = formalErrorTerm(error);
2985
+ assertEqual(first === second, true, 'reused ground error term');
2986
+ },
2987
+ },
2962
2988
  {
2963
2989
  name: 'caught number syntax errors do not exhaust memory on distinct inputs',
2964
2990
  run: () => {
@@ -2979,22 +3005,22 @@ open(X) :- candidate(X), \\+ closed(X).
2979
3005
  const goal = parseGoalText('trial(Chars)');
2980
3006
  let count = 0;
2981
3007
  for (const _ of solver.solve([goal], new Env(), 0)) {
2982
- if (++count === 50000) break;
3008
+ if (++count === 250000) break;
2983
3009
  }
2984
- if (count !== 50000) throw new Error('unexpected answer count: ' + count);
3010
+ if (count !== 250000) throw new Error('unexpected answer count: ' + count);
2985
3011
  process.stdout.write(String(count));
2986
3012
  `;
2987
3013
  const result = spawnSync(process.execPath, [
2988
- // A smaller heap preserves the original false-exhaustion signal
2989
- // without requiring half a million transient conversion attempts.
3014
+ // Run well past the roughly 126,000-answer failure reported in #28
3015
+ // while keeping the host heap deliberately constrained.
2990
3016
  '--max-old-space-size=32',
2991
3017
  '--input-type=module',
2992
3018
  '--eval',
2993
3019
  script,
2994
- ], { cwd: packageRoot, encoding: 'utf8', timeout: 10000 });
3020
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
2995
3021
  if (result.error) throw result.error;
2996
3022
  assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
2997
- assertEqual(result.stdout, '50000', 'distinct number syntax attempts');
3023
+ assertEqual(result.stdout, '250000', 'distinct number syntax attempts');
2998
3024
  },
2999
3025
  },
3000
3026
  {
@@ -4633,7 +4633,11 @@ testing is a powerful guard during program transformation.
4633
4633
  `--stats` reports work, not meaning. A high solution count may be necessary or
4634
4634
  may indicate a generator that should be constrained. Many table hits may show
4635
4635
  effective reuse; many distinct table entries may reveal an argument that
4636
- prevents calls from sharing.
4636
+ prevents calls from sharing. On the Node CLI it also reports current heap use,
4637
+ non-young/old-generation use, the amount currently compared with the memory
4638
+ guard, resident-set size, and the soft and hard memory ceilings in bytes. These memory figures are printed even when execution ends by
4639
+ raising a Prolog error, which makes `--stats` useful when reproducing bounded
4640
+ memory failures such as long-running conversion stress tests.
4637
4641
 
4638
4642
  Compare statistics only between runs with the same query, data, and observable
4639
4643
  answer contract. A faster program that silently loses answers is not an