eyeprolog 1.2.18 → 1.2.19
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/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/platform.js +42 -0
- package/src/solver.js +16 -0
- package/test/run-regression.mjs +29 -0
- package/the-art-of-eyeprolog.md +29 -16
package/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface EyePrologRunOptions {
|
|
|
12
12
|
explain?: boolean;
|
|
13
13
|
maxDepth?: number;
|
|
14
14
|
maxInferences?: number;
|
|
15
|
+
/** Soft JavaScript heap ceiling in bytes; exhaustion raises resource_error(memory). */
|
|
16
|
+
maxMemoryBytes?: number;
|
|
15
17
|
solutionLimit?: number;
|
|
16
18
|
registry?: BuiltinRegistry;
|
|
17
19
|
sourceMetadata?: boolean;
|
|
@@ -171,6 +173,7 @@ export class Solver {
|
|
|
171
173
|
maxInferences: number;
|
|
172
174
|
inferences: number;
|
|
173
175
|
inferenceLimitExceeded: boolean;
|
|
176
|
+
maxMemoryBytes: number;
|
|
174
177
|
solutionLimit: number;
|
|
175
178
|
solutionsSeen: number;
|
|
176
179
|
active: unknown[];
|
package/package.json
CHANGED
package/src/platform.js
CHANGED
|
@@ -6,10 +6,12 @@ const isNode = typeof process !== 'undefined' && Boolean(process.versions?.node)
|
|
|
6
6
|
let fs = null;
|
|
7
7
|
let path = null;
|
|
8
8
|
let BufferCtor = null;
|
|
9
|
+
let v8 = null;
|
|
9
10
|
|
|
10
11
|
if (isNode) {
|
|
11
12
|
({ default: fs } = await import('node:fs'));
|
|
12
13
|
({ default: path } = await import('node:path'));
|
|
14
|
+
({ default: v8 } = await import('node:v8'));
|
|
13
15
|
BufferCtor = globalThis.Buffer ?? null;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -18,3 +20,43 @@ export { fs, path, BufferCtor, isNode };
|
|
|
18
20
|
export function currentWorkingDirectory() {
|
|
19
21
|
return isNode && typeof process.cwd === 'function' ? process.cwd() : '/';
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
export function usedHeapSize() {
|
|
25
|
+
if (isNode && typeof process.memoryUsage === 'function') {
|
|
26
|
+
return process.memoryUsage().heapUsed;
|
|
27
|
+
}
|
|
28
|
+
const memory = globalThis.performance?.memory;
|
|
29
|
+
return Number.isFinite(memory?.usedJSHeapSize) ? memory.usedJSHeapSize : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function softHeapLimit() {
|
|
33
|
+
let limit = null;
|
|
34
|
+
if (isNode) {
|
|
35
|
+
limit = v8?.getHeapStatistics?.().heap_size_limit ?? null;
|
|
36
|
+
const configuredOldSpace = configuredOldSpaceBytes();
|
|
37
|
+
if (configuredOldSpace != null) limit = Math.min(limit ?? Infinity, configuredOldSpace);
|
|
38
|
+
} else {
|
|
39
|
+
const memory = globalThis.performance?.memory;
|
|
40
|
+
if (Number.isFinite(memory?.jsHeapSizeLimit)) limit = memory.jsHeapSizeLimit;
|
|
41
|
+
}
|
|
42
|
+
// Leave ample room for the generator stack to unwind and for the top level
|
|
43
|
+
// to construct and print resource_error(memory). Fatal V8 OOMs cannot be
|
|
44
|
+
// caught after the heap limit itself has been reached.
|
|
45
|
+
return Number.isFinite(limit) && limit > 0 ? Math.floor(limit * 0.75) : Infinity;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function configuredOldSpaceBytes() {
|
|
49
|
+
if (!isNode) return null;
|
|
50
|
+
const argumentsText = [
|
|
51
|
+
...(process.execArgv ?? []),
|
|
52
|
+
...(String(process.env?.NODE_OPTIONS ?? '').match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []),
|
|
53
|
+
];
|
|
54
|
+
for (let index = 0; index < argumentsText.length; index++) {
|
|
55
|
+
const argument = argumentsText[index];
|
|
56
|
+
const match = /^--max[-_]old[-_]space[-_]size(?:=(\d+))?$/.exec(argument);
|
|
57
|
+
if (!match) continue;
|
|
58
|
+
const megabytes = match[1] ?? argumentsText[index + 1];
|
|
59
|
+
if (/^\d+$/.test(megabytes ?? '')) return Number(megabytes) * 1024 * 1024;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
package/src/solver.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getEyePrologRegistry } from './standard-library.js';
|
|
|
9
9
|
import { selectClauseCandidates, selectClauseCandidatesForValues, selectGroundClauseCandidates } from './program.js';
|
|
10
10
|
import { StreamManager } from './io.js';
|
|
11
11
|
import { clpzStateConsistent } from './clpz.js';
|
|
12
|
+
import { softHeapLimit, usedHeapSize } from './platform.js';
|
|
12
13
|
|
|
13
14
|
let freshCounter = 0;
|
|
14
15
|
|
|
@@ -47,6 +48,8 @@ export class Solver {
|
|
|
47
48
|
this.maxInferences = options.maxInferences ?? Infinity;
|
|
48
49
|
this.inferences = 0;
|
|
49
50
|
this.inferenceLimitExceeded = false;
|
|
51
|
+
this.maxMemoryBytes = options.maxMemoryBytes ?? softHeapLimit();
|
|
52
|
+
this.nextMemoryCheck = 0;
|
|
50
53
|
// Do not impose an implicit answer cap. Infinite and very large searches are
|
|
51
54
|
// part of normal Prolog semantics; callers that need a resource bound can
|
|
52
55
|
// still supply solutionLimit explicitly.
|
|
@@ -115,6 +118,7 @@ export class Solver {
|
|
|
115
118
|
registry: this.registry,
|
|
116
119
|
maxDepth: this.maxDepth,
|
|
117
120
|
maxInferences: this.maxInferences,
|
|
121
|
+
maxMemoryBytes: this.maxMemoryBytes,
|
|
118
122
|
solutionLimit,
|
|
119
123
|
isoStrict: this.isoStrict,
|
|
120
124
|
prologFlags: this.prologFlags,
|
|
@@ -177,6 +181,7 @@ export class Solver {
|
|
|
177
181
|
this.solveStacks.push(stack);
|
|
178
182
|
while (stack.length) {
|
|
179
183
|
this.inferences++;
|
|
184
|
+
this.checkMemoryLimit();
|
|
180
185
|
if (this.inferences > this.maxInferences) {
|
|
181
186
|
this.inferenceLimitExceeded = true;
|
|
182
187
|
break;
|
|
@@ -228,6 +233,7 @@ export class Solver {
|
|
|
228
233
|
|
|
229
234
|
while (true) {
|
|
230
235
|
this.inferences++;
|
|
236
|
+
this.checkMemoryLimit();
|
|
231
237
|
if (this.inferences > this.maxInferences) {
|
|
232
238
|
this.inferenceLimitExceeded = true;
|
|
233
239
|
stack.length = 0;
|
|
@@ -411,6 +417,16 @@ export class Solver {
|
|
|
411
417
|
return activeVariantIn(goal, env, this.active);
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
checkMemoryLimit() {
|
|
421
|
+
if (this.inferences < this.nextMemoryCheck) return;
|
|
422
|
+
this.nextMemoryCheck = this.inferences + 256;
|
|
423
|
+
if (!Number.isFinite(this.maxMemoryBytes)) return;
|
|
424
|
+
const used = usedHeapSize();
|
|
425
|
+
if (used != null && used >= this.maxMemoryBytes) {
|
|
426
|
+
throw new PrologError('resource_error(memory)');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
414
430
|
*solveUserGoal(goal, rest, env, depth) {
|
|
415
431
|
this.stats.solve_one_goal_calls++;
|
|
416
432
|
if (depth > this.maxDepth) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -2324,6 +2324,35 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2324
2324
|
}
|
|
2325
2325
|
},
|
|
2326
2326
|
},
|
|
2327
|
+
{
|
|
2328
|
+
name: 'list allocation heap pressure becomes resource_error(memory)',
|
|
2329
|
+
run: () => {
|
|
2330
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2331
|
+
const programText = ':- use_module(library(prologue)).\n';
|
|
2332
|
+
const goalText = 'length(_, I), I > 9, N is 2^I, \\+ \\+ length(_, N)';
|
|
2333
|
+
const script = `
|
|
2334
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2335
|
+
const program = Program.parse(${JSON.stringify(programText)});
|
|
2336
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2337
|
+
const goal = parseGoalText(${JSON.stringify(goalText)}, {
|
|
2338
|
+
operatorDefinitions: [...program.operators.values()],
|
|
2339
|
+
});
|
|
2340
|
+
let caught = null;
|
|
2341
|
+
try { [...solver.solve([goal], new Env(), 0)]; } catch (error) { caught = error; }
|
|
2342
|
+
if (caught?.formal !== 'resource_error(memory)') throw caught ?? new Error('no resource error');
|
|
2343
|
+
process.stdout.write(caught.formal);
|
|
2344
|
+
`;
|
|
2345
|
+
const result = spawnSync(process.execPath, [
|
|
2346
|
+
'--max-old-space-size=64',
|
|
2347
|
+
'--input-type=module',
|
|
2348
|
+
'--eval',
|
|
2349
|
+
script,
|
|
2350
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2351
|
+
if (result.error) throw result.error;
|
|
2352
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2353
|
+
assertEqual(result.stdout, 'resource_error(memory)', 'heap pressure resource error');
|
|
2354
|
+
},
|
|
2355
|
+
},
|
|
2327
2356
|
{
|
|
2328
2357
|
name: 'solver honors solution limits',
|
|
2329
2358
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1725,6 +1725,8 @@ const result = run(reasoningSource(record), {
|
|
|
1725
1725
|
goal: `thermal_alert(${record.sensor})`,
|
|
1726
1726
|
proof: true,
|
|
1727
1727
|
maxDepth: 10_000,
|
|
1728
|
+
maxInferences: 100_000,
|
|
1729
|
+
maxMemoryBytes: 256 * 1024 * 1024,
|
|
1728
1730
|
solutionLimit: 10
|
|
1729
1731
|
});
|
|
1730
1732
|
|
|
@@ -1774,10 +1776,10 @@ prints numeric work counters; those counters describe this run rather than an
|
|
|
1774
1776
|
additional logical answer.
|
|
1775
1777
|
|
|
1776
1778
|
`run/2` accepts source text or an already parsed `Program`. Its options include
|
|
1777
|
-
`proof` (with `why` and `explain` as aliases), `maxDepth`, `
|
|
1778
|
-
custom `registry`, and `strictNegation` or
|
|
1779
|
-
`stdout`, the solver's numeric `stats`, and a
|
|
1780
|
-
write to the process streams.
|
|
1779
|
+
`proof` (with `why` and `explain` as aliases), `maxDepth`, `maxInferences`,
|
|
1780
|
+
`maxMemoryBytes`, `solutionLimit`, a custom `registry`, and `strictNegation` or
|
|
1781
|
+
`analyzeNegation`. It returns `stdout`, the solver's numeric `stats`, and a
|
|
1782
|
+
nullable `haltCode`; it does not write to the process streams.
|
|
1781
1783
|
|
|
1782
1784
|
For applications that inspect or prepare a theory before running it, use
|
|
1783
1785
|
`Program` directly:
|
|
@@ -1800,18 +1802,24 @@ console.log(path?.recursive, path?.tabled, path?.tableInputPositions);
|
|
|
1800
1802
|
|
|
1801
1803
|
const solver = new Solver(program, {
|
|
1802
1804
|
maxDepth: 50_000,
|
|
1805
|
+
maxInferences: 1_000_000,
|
|
1806
|
+
maxMemoryBytes: 256 * 1024 * 1024,
|
|
1803
1807
|
solutionLimit: 100_000
|
|
1804
1808
|
});
|
|
1805
1809
|
```
|
|
1806
1810
|
|
|
1807
|
-
The limits are safety ceilings, not logical declarations. Reaching
|
|
1808
|
-
truncate search; it does not prove that no
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1811
|
+
The limits are safety ceilings, not logical declarations. Reaching the depth,
|
|
1812
|
+
inference, or solution ceiling may truncate search; it does not prove that no
|
|
1813
|
+
further answer exists. Reaching `maxMemoryBytes` instead raises
|
|
1814
|
+
`resource_error(memory)`, because continuing until the JavaScript engine's hard
|
|
1815
|
+
heap limit would let the host abort before Prolog could report an exception. At
|
|
1816
|
+
the `Solver` API boundary, `solutionLimit` is opt-in: if it is omitted, ordinary
|
|
1817
|
+
solving and child searches that inherit the solver limit do not stop after a
|
|
1818
|
+
fixed number of solutions. This matters for re-executable goals such as
|
|
1819
|
+
`repeat/0` and for library relations such as `call_nth/2`; an implementation
|
|
1820
|
+
safety threshold must not turn a still re-executable search into logical
|
|
1821
|
+
failure. Embedders that need a finite answer budget should pass `solutionLimit`
|
|
1822
|
+
explicitly.
|
|
1815
1823
|
|
|
1816
1824
|
Variable term order is deliberately scoped rather than stored as a permanent
|
|
1817
1825
|
property of a variable. ISO 13211-1 section 7.2.1 leaves the order of two
|
|
@@ -1822,10 +1830,15 @@ sorting step of `setof/3` share one ranking for the duration of that single
|
|
|
1822
1830
|
sorted-list operation. No process-global variable registry or creation ordinal
|
|
1823
1831
|
is retained or exposed through later comparisons.
|
|
1824
1832
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
`resource_error(memory)`
|
|
1828
|
-
|
|
1833
|
+
EyeProlog periodically checks detectable JavaScript heap use and keeps a quarter
|
|
1834
|
+
of the host heap ceiling in reserve so the solver can unwind and report
|
|
1835
|
+
`resource_error(memory)` before a fatal host out-of-memory abort. Embedders may
|
|
1836
|
+
replace that automatically derived soft ceiling with `maxMemoryBytes`; setting
|
|
1837
|
+
it to `Infinity` disables the proactive check. Environments that do not expose
|
|
1838
|
+
heap use cannot provide the proactive check. Host capacity failures that V8
|
|
1839
|
+
reports as `Map maximum size exceeded` or `Set maximum size exceeded` are also
|
|
1840
|
+
normalized at the solver boundary instead of leaking a JavaScript `RangeError`.
|
|
1841
|
+
ISO 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
|
|
1829
1842
|
`memory` for a finite host allocation/capacity ceiling and reserves the
|
|
1830
1843
|
`finite_memory` spelling for the distinct convention where no finite amount of
|
|
1831
1844
|
memory could complete the computation.
|