eyeprolog 1.2.41 → 1.2.43
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/src/standard-library.js +39 -2
- package/test/run-regression.mjs +68 -8
- package/the-art-of-eyeprolog.md +17 -2
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/src/standard-library.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// The sources are registered here so library(Name) works in Node and browsers;
|
|
3
3
|
// unlike the former autoloader, no clauses are added unless use_module/1 or
|
|
4
4
|
// use_module/2 requests their module.
|
|
5
|
-
import { createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
5
|
+
import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
6
6
|
import { clpzBuiltins } from './clpz.js';
|
|
7
|
-
import { fs, isNode } from './platform.js';
|
|
7
|
+
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
8
|
+
import { ATOM, VAR, atom, deref, numberTerm, unify } from './term.js';
|
|
8
9
|
|
|
9
10
|
const moduleFiles = Object.freeze({
|
|
10
11
|
aggregate: 'aggregate.pl',
|
|
@@ -73,8 +74,44 @@ export const eyePrologLibraryIndicators = Object.freeze([
|
|
|
73
74
|
...eyePrologNativeLibraryIndicators,
|
|
74
75
|
]);
|
|
75
76
|
|
|
77
|
+
function runtimeStatistics(solver) {
|
|
78
|
+
return { ...solver.stats, ...memoryStatistics() };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function* statisticsBuiltin({ solver, env }) {
|
|
82
|
+
const stream = solver.io.resolve(solver.io.currentOutput);
|
|
83
|
+
if (stream?.type !== 'text') throw new PrologError('permission_error(output, binary_stream)');
|
|
84
|
+
solver.io.writeUnit(stream, 'eyeprolog stats:\n');
|
|
85
|
+
for (const [key, value] of Object.entries(runtimeStatistics(solver))) {
|
|
86
|
+
solver.io.writeUnit(stream, ` ${key}: ${value}\n`);
|
|
87
|
+
}
|
|
88
|
+
yield env;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function* statisticsValueBuiltin({ solver, goal, env }) {
|
|
92
|
+
const snapshot = runtimeStatistics(solver);
|
|
93
|
+
const key = deref(goal.args[0], env);
|
|
94
|
+
const entries = key.type === VAR
|
|
95
|
+
? Object.entries(snapshot)
|
|
96
|
+
: key.type === ATOM && Object.hasOwn(snapshot, key.name)
|
|
97
|
+
? [[key.name, snapshot[key.name]]]
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
if (entries == null) {
|
|
101
|
+
if (key.type !== ATOM) throw new PrologError('type_error(atom)', key);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [name, value] of entries) {
|
|
106
|
+
const next = env.clone();
|
|
107
|
+
if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], numberTerm(value), next)) yield next;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
76
111
|
export function createEyePrologRegistry() {
|
|
77
112
|
const registry = createDefaultRegistry();
|
|
113
|
+
registry.add('statistics', 0, statisticsBuiltin, { deterministic: true });
|
|
114
|
+
registry.add('statistics', 2, statisticsValueBuiltin);
|
|
78
115
|
eyePrologLibraryBuiltins.register(registry);
|
|
79
116
|
clpzBuiltins.register(registry);
|
|
80
117
|
registry.eyePrologLibrary = true;
|
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
|
{
|
|
@@ -1958,6 +1975,36 @@ c4 ?- call((!;1)).
|
|
|
1958
1975
|
assertIncludes(result.stderr, ' solve_goals_calls:', 'stderr');
|
|
1959
1976
|
},
|
|
1960
1977
|
},
|
|
1978
|
+
{
|
|
1979
|
+
name: 'statistics/0 prints snapshots during execution',
|
|
1980
|
+
run: () => {
|
|
1981
|
+
const input = '%% goal: live\nlive :- statistics, statistics.\n';
|
|
1982
|
+
const result = runCli(['-'], { input });
|
|
1983
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1984
|
+
assertEqual((result.stdout.match(/eyeprolog stats:/g) ?? []).length, 2, 'in-run snapshot count');
|
|
1985
|
+
assertIncludes(result.stdout, ' memory_guard_used_bytes:', 'stdout');
|
|
1986
|
+
assertIncludes(result.stdout, 'live.\n', 'stdout');
|
|
1987
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1988
|
+
},
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
name: 'statistics/2 exposes current counters and memory values',
|
|
1992
|
+
run: () => {
|
|
1993
|
+
const input = '%% goal: live(Used)\nlive(Used) :- statistics(memory_guard_used_bytes, Used).\n';
|
|
1994
|
+
const result = runCli(['-'], { input });
|
|
1995
|
+
assertEqual(result.status, 0, 'exit status');
|
|
1996
|
+
assertEqual(/^live\(\d+\)\.\n$/.test(result.stdout), true, 'numeric memory statistic');
|
|
1997
|
+
assertEqual(result.stderr, '', 'stderr');
|
|
1998
|
+
},
|
|
1999
|
+
},
|
|
2000
|
+
{
|
|
2001
|
+
name: 'statistics predicates are excluded from strict ISO mode',
|
|
2002
|
+
run: () => {
|
|
2003
|
+
const result = runCli(['--iso-strict', '-'], { input: '%% goal: statistics\n' });
|
|
2004
|
+
assertEqual(result.status, 1, 'exit status');
|
|
2005
|
+
assertIncludes(result.stderr, 'existence_error(procedure)', 'stderr');
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
1961
2008
|
{
|
|
1962
2009
|
name: '--warnings prints unstratified negation diagnostics without failing',
|
|
1963
2010
|
run: () => {
|
|
@@ -2959,6 +3006,15 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2959
3006
|
assertEqual(result.stdout, '300000', 'fresh-variable answer count');
|
|
2960
3007
|
},
|
|
2961
3008
|
},
|
|
3009
|
+
{
|
|
3010
|
+
name: 'ground Prolog error terms are reused across catches',
|
|
3011
|
+
run: () => {
|
|
3012
|
+
const error = new PrologError('syntax_error(number)');
|
|
3013
|
+
const first = formalErrorTerm(error);
|
|
3014
|
+
const second = formalErrorTerm(error);
|
|
3015
|
+
assertEqual(first === second, true, 'reused ground error term');
|
|
3016
|
+
},
|
|
3017
|
+
},
|
|
2962
3018
|
{
|
|
2963
3019
|
name: 'caught number syntax errors do not exhaust memory on distinct inputs',
|
|
2964
3020
|
run: () => {
|
|
@@ -2979,22 +3035,22 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2979
3035
|
const goal = parseGoalText('trial(Chars)');
|
|
2980
3036
|
let count = 0;
|
|
2981
3037
|
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2982
|
-
if (++count ===
|
|
3038
|
+
if (++count === 250000) break;
|
|
2983
3039
|
}
|
|
2984
|
-
if (count !==
|
|
3040
|
+
if (count !== 250000) throw new Error('unexpected answer count: ' + count);
|
|
2985
3041
|
process.stdout.write(String(count));
|
|
2986
3042
|
`;
|
|
2987
3043
|
const result = spawnSync(process.execPath, [
|
|
2988
|
-
//
|
|
2989
|
-
//
|
|
3044
|
+
// Run well past the roughly 126,000-answer failure reported in #28
|
|
3045
|
+
// while keeping the host heap deliberately constrained.
|
|
2990
3046
|
'--max-old-space-size=32',
|
|
2991
3047
|
'--input-type=module',
|
|
2992
3048
|
'--eval',
|
|
2993
3049
|
script,
|
|
2994
|
-
], { cwd: packageRoot, encoding: 'utf8', timeout:
|
|
3050
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2995
3051
|
if (result.error) throw result.error;
|
|
2996
3052
|
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2997
|
-
assertEqual(result.stdout, '
|
|
3053
|
+
assertEqual(result.stdout, '250000', 'distinct number syntax attempts');
|
|
2998
3054
|
},
|
|
2999
3055
|
},
|
|
3000
3056
|
{
|
|
@@ -3179,9 +3235,13 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
3179
3235
|
assertEqual(Boolean(registry.get('is', 2)), true, 'ISO is/2 exists');
|
|
3180
3236
|
assertEqual(Boolean(registry.get('append', 3)), false, 'append/3 is not ISO core');
|
|
3181
3237
|
assertEqual(library.eyePrologLibrary, true, 'complete registry marker');
|
|
3182
|
-
assertEqual(library.defs.size,
|
|
3238
|
+
assertEqual(library.defs.size, 155, 'EyeProlog registry contains ISO definitions, observability extensions, and private library adapters');
|
|
3183
3239
|
assertEqual(Boolean(registry.get('phrase', 2)), true, 'Part 3 phrase/2 exists');
|
|
3184
3240
|
assertEqual(Boolean(registry.get('phrase', 3)), true, 'Part 3 phrase/3 exists');
|
|
3241
|
+
assertEqual(registry.get('statistics', 0), null, 'statistics/0 is absent from the ISO registry');
|
|
3242
|
+
assertEqual(registry.get('statistics', 2), null, 'statistics/2 is absent from the ISO registry');
|
|
3243
|
+
assertEqual(Boolean(library.get('statistics', 0)), true, 'statistics/0 is an EyeProlog observability extension');
|
|
3244
|
+
assertEqual(Boolean(library.get('statistics', 2)), true, 'statistics/2 is an EyeProlog observability extension');
|
|
3185
3245
|
assertEqual(registeredNativeEyePrologLibraryNames().length, 40, 'public native EyeProlog builtin count');
|
|
3186
3246
|
assertEqual(eyePrologPortableLibraryIndicators.length, 59, 'portable Prolog library count');
|
|
3187
3247
|
assertEqual(eyePrologNativeLibraryIndicators.length, 40, 'native host library count');
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -4633,7 +4633,22 @@ 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
|
|
4639
|
+
memory figures are printed even when execution ends by raising a Prolog error.
|
|
4640
|
+
|
|
4641
|
+
`--stats` is an end-of-run summary. For a deliberately non-terminating or very
|
|
4642
|
+
long computation, call the EyeProlog extension `statistics/0` at the points
|
|
4643
|
+
where a live snapshot is useful. For example, a long-running loop can include
|
|
4644
|
+
`statistics` as one of its goals, as in `loop :- work, statistics, loop.`
|
|
4645
|
+
|
|
4646
|
+
`statistics/0` writes the current solver counters and memory figures
|
|
4647
|
+
immediately to the current output stream. `statistics/2` makes an individual
|
|
4648
|
+
value available to the program, for example
|
|
4649
|
+
`statistics(memory_guard_used_bytes, Used)`. With an unbound first argument it
|
|
4650
|
+
enumerates the available statistic keys and values. These predicates are
|
|
4651
|
+
EyeProlog observability extensions and are not available under `--iso-strict`.
|
|
4637
4652
|
|
|
4638
4653
|
Compare statistics only between runs with the same query, data, and observable
|
|
4639
4654
|
answer contract. A faster program that silently loses answers is not an
|
|
@@ -6406,7 +6421,7 @@ make the observed question explicit.
|
|
|
6406
6421
|
| `-p`, `--proof` | Print `why/2` explanations |
|
|
6407
6422
|
| `-q`, `--quads` | Run embedded quad tests and fail if any do not hold |
|
|
6408
6423
|
| `--iso-strict` | Restrict parsing and execution to ISO/IEC 13211-1:1995 + Corrigenda 1–3; reject EyeProlog language extensions and disable automatic tabling |
|
|
6409
|
-
| `-s`, `--stats` | Print solver
|
|
6424
|
+
| `-s`, `--stats` | Print final solver and memory statistics to stderr after execution |
|
|
6410
6425
|
| `-v`, `--version` | Print the package version |
|
|
6411
6426
|
| `-w`, `--warnings` | Print non-fatal portability warnings |
|
|
6412
6427
|
| `-g`, `--goal Goal` | Solve a callable goal; may be repeated; overrides `%% goal:` comments |
|