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 +1 -1
- package/src/cli.js +5 -4
- package/src/iso.js +50 -26
- package/src/platform.js +35 -5
- package/test/run-regression.mjs +33 -7
- package/the-art-of-eyeprolog.md +5 -1
package/package.json
CHANGED
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
|
|
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
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
const
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
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
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
34
|
-
const
|
|
35
|
-
return
|
|
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
|
package/test/run-regression.mjs
CHANGED
|
@@ -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 ===
|
|
3008
|
+
if (++count === 250000) break;
|
|
2983
3009
|
}
|
|
2984
|
-
if (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
|
-
//
|
|
2989
|
-
//
|
|
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:
|
|
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, '
|
|
3023
|
+
assertEqual(result.stdout, '250000', 'distinct number syntax attempts');
|
|
2998
3024
|
},
|
|
2999
3025
|
},
|
|
3000
3026
|
{
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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
|