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 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
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.18",
6
+ "version": "1.2.19",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
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) {
@@ -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: () => {
@@ -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`, `solutionLimit`, a
1778
- custom `registry`, and `strictNegation` or `analyzeNegation`. It returns
1779
- `stdout`, the solver's numeric `stats`, and a nullable `haltCode`; it does not
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 one may
1808
- truncate search; it does not prove that no further answer exists. At the `Solver`
1809
- API boundary, `solutionLimit` is opt-in: if it is omitted, ordinary solving and
1810
- child searches that inherit the solver limit do not stop after a fixed number of
1811
- solutions. This matters for re-executable goals such as `repeat/0` and for
1812
- library relations such as `call_nth/2`; an implementation safety threshold must
1813
- not turn a still re-executable search into logical failure. Embedders that need
1814
- a finite answer budget should pass `solutionLimit` explicitly.
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
- Host capacity failures that V8 reports as `Map maximum size exceeded` or `Set`
1826
- `maximum size exceeded` are normalized at the solver boundary to
1827
- `resource_error(memory)` instead of leaking a JavaScript `RangeError`. ISO
1828
- 13211-1 leaves the resource atom implementation dependent. EyeProlog uses
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.