eyeprolog 1.2.26 → 1.2.28

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.26",
6
+ "version": "1.2.28",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -1144,6 +1144,43 @@ function convertedTermText(text, solver) {
1144
1144
  }
1145
1145
  return result;
1146
1146
  }
1147
+
1148
+ function scopeReadTerm(term) {
1149
+ // A term read from a stream has its own variable set (ISO 7.10.3). Parser
1150
+ // variable names cannot be used as environment identities here: a caller
1151
+ // such as read(X) and an input term X=a would otherwise share the same `X`
1152
+ // and incorrectly attempt the cyclic unification X=(X=a). Use an internal
1153
+ // name containing NUL, which cannot occur in Prolog source, while retaining
1154
+ // the spelling and occurrence count required by read_term/3 metadata.
1155
+ const scope = ++isoFresh;
1156
+ const bySourceName = new Map();
1157
+ const variables = [];
1158
+
1159
+ const copy = (item) => {
1160
+ if (item.type === VAR) {
1161
+ let record = bySourceName.get(item.name);
1162
+ if (record == null) {
1163
+ const scoped = variable(`\u0000read:${scope}:${variables.length}`);
1164
+ scoped.displayName = item.name;
1165
+ record = {
1166
+ sourceName: item.name,
1167
+ term: scoped,
1168
+ count: 0,
1169
+ anonymous: item.name.startsWith('__anon'),
1170
+ };
1171
+ bySourceName.set(item.name, record);
1172
+ variables.push(record);
1173
+ }
1174
+ record.count++;
1175
+ return record.term;
1176
+ }
1177
+ if (item.type !== COMPOUND) return item;
1178
+ return compound(item.name, item.args.map(copy));
1179
+ };
1180
+
1181
+ return { term: copy(term), variables };
1182
+ }
1183
+
1147
1184
  function readTermFromStream(stream, solver) {
1148
1185
  let requestedInteractiveTerm = false;
1149
1186
  while (true) {
@@ -1164,7 +1201,7 @@ function readTermFromStream(stream, solver) {
1164
1201
  });
1165
1202
  if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
1166
1203
  stream.position = candidate.end;
1167
- return clauses[0].head;
1204
+ return scopeReadTerm(clauses[0].head);
1168
1205
  } catch (_) {
1169
1206
  // A dot inside a graphic operator, such as =.., is only a possible
1170
1207
  // terminator. Keep scanning until a complete term parses.
@@ -1192,7 +1229,7 @@ function readTermFromStream(stream, solver) {
1192
1229
  stream.position = source.length;
1193
1230
  if (!sawCandidate) {
1194
1231
  if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
1195
- return atom('end_of_file');
1232
+ return { term: atom('end_of_file'), variables: [] };
1196
1233
  }
1197
1234
  throw new PrologError('syntax_error(read_term)');
1198
1235
  }
@@ -1202,39 +1239,31 @@ function* readBuiltin({ solver, goal, env }) {
1202
1239
  const stream = inputStreamFor(solver, goal, env);
1203
1240
  if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1204
1241
  const next = env.clone();
1205
- if (unify(goal.args[goal.arity - 1], readTermFromStream(stream, solver), next)) yield next;
1242
+ const { term } = readTermFromStream(stream, solver);
1243
+ if (unify(goal.args[goal.arity - 1], term, next)) yield next;
1206
1244
  }
1207
1245
  function* readTermBuiltin({ solver, goal, env }) {
1208
1246
  const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
1209
1247
  if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1210
1248
  const options = optionList(goal.args[goal.arity - 1], env);
1211
1249
  const target = goal.args[goal.arity - 2];
1212
- const term = readTermFromStream(stream, solver);
1250
+ const { term, variables } = readTermFromStream(stream, solver);
1213
1251
  const next = env.clone();
1214
1252
  if (!unify(target, term, next)) return;
1215
- const variables = [];
1216
- const counts = new Map();
1217
- const visit = (item) => {
1218
- if (item.type === VAR) {
1219
- counts.set(item.name, (counts.get(item.name) ?? 0) + 1);
1220
- if (!variables.some((entry) => entry.name === item.name)) variables.push(item);
1221
- } else for (const arg of item.args) visit(arg);
1222
- };
1223
- visit(term);
1224
1253
  for (const option of options) {
1225
1254
  if (option.type === VAR) throw new PrologError('instantiation_error');
1226
1255
  if (option.type !== COMPOUND || option.arity !== 1) throw new PrologError('domain_error(read_option)', option);
1227
1256
  let value;
1228
1257
  if (option.name === 'variables') {
1229
- value = listFromItems(variables);
1258
+ value = listFromItems(variables.map((item) => item.term));
1230
1259
  } else if (option.name === 'variable_names') {
1231
1260
  value = listFromItems(variables
1232
- .filter((item) => !item.name.startsWith('__anon'))
1233
- .map((item) => compound('=', [atom(item.name), item])));
1261
+ .filter((item) => !item.anonymous)
1262
+ .map((item) => compound('=', [atom(item.sourceName), item.term])));
1234
1263
  } else if (option.name === 'singletons') {
1235
1264
  value = listFromItems(variables
1236
- .filter((item) => !item.name.startsWith('__anon') && counts.get(item.name) === 1)
1237
- .map((item) => compound('=', [atom(item.name), item])));
1265
+ .filter((item) => !item.anonymous && item.count === 1)
1266
+ .map((item) => compound('=', [atom(item.sourceName), item.term])));
1238
1267
  } else {
1239
1268
  throw new PrologError('domain_error(read_option)', option);
1240
1269
  }
package/src/platform.js CHANGED
@@ -15,6 +15,8 @@ if (isNode) {
15
15
  BufferCtor = globalThis.Buffer ?? null;
16
16
  }
17
17
 
18
+ const configuredOldSpaceLimit = configuredOldSpaceBytes();
19
+
18
20
  export { fs, path, BufferCtor, isNode };
19
21
 
20
22
  export function currentWorkingDirectory() {
@@ -23,6 +25,17 @@ export function currentWorkingDirectory() {
23
25
 
24
26
  export function usedHeapSize() {
25
27
  if (isNode && typeof process.memoryUsage === 'function') {
28
+ // --max-old-space-size constrains V8's old generation, not the complete
29
+ // heap reported by process.memoryUsage().heapUsed. Comparing that limit
30
+ // with total heap use makes bursts of collectible new-space objects look
31
+ // like retained memory. Measure the corresponding non-young spaces when
32
+ // 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);
38
+ }
26
39
  return process.memoryUsage().heapUsed;
27
40
  }
28
41
  const memory = globalThis.performance?.memory;
@@ -33,8 +46,7 @@ export function softHeapLimit() {
33
46
  let limit = null;
34
47
  if (isNode) {
35
48
  limit = v8?.getHeapStatistics?.().heap_size_limit ?? null;
36
- const configuredOldSpace = configuredOldSpaceBytes();
37
- if (configuredOldSpace != null) limit = Math.min(limit ?? Infinity, configuredOldSpace);
49
+ if (configuredOldSpaceLimit != null) limit = Math.min(limit ?? Infinity, configuredOldSpaceLimit);
38
50
  } else {
39
51
  const memory = globalThis.performance?.memory;
40
52
  if (Number.isFinite(memory?.jsHeapSizeLimit)) limit = memory.jsHeapSizeLimit;
package/src/term.js CHANGED
@@ -413,9 +413,26 @@ function writeList(term, env, options) {
413
413
  }
414
414
 
415
415
  export function termToString(term, env = new Env(), quoteStrings = true, options = {}) {
416
- options = { doubleQuotes: options.doubleQuotes ?? 'chars' };
416
+ options = {
417
+ ...options,
418
+ doubleQuotes: options.doubleQuotes ?? 'chars',
419
+ readVariableNames: options.readVariableNames instanceof Map ? options.readVariableNames : new Map(),
420
+ usedReadVariableNames: options.usedReadVariableNames instanceof Set ? options.usedReadVariableNames : new Set(),
421
+ };
417
422
  const resolved = deref(term, env);
418
- if (resolved.type === VAR) return writeVariable(resolved.name);
423
+ if (resolved.type === VAR) {
424
+ if (resolved.displayName == null) return writeVariable(resolved.name);
425
+ let printed = options.readVariableNames.get(resolved.name);
426
+ if (printed == null) {
427
+ const base = writeVariable(resolved.displayName);
428
+ printed = base;
429
+ let suffix = 1;
430
+ while (options.usedReadVariableNames.has(printed)) printed = `${base}_${suffix++}`;
431
+ options.readVariableNames.set(resolved.name, printed);
432
+ options.usedReadVariableNames.add(printed);
433
+ }
434
+ return printed;
435
+ }
419
436
  if (isCons(resolved)) return writeList(resolved, env, options);
420
437
  if (resolved.type === STRING) return writeString(resolved.name, quoteStrings);
421
438
  if (resolved.type === ATOM) return writeAtom(resolved.name);
package/src/write.js CHANGED
@@ -134,9 +134,42 @@ function chooseOperator(term, table) {
134
134
  return null;
135
135
  }
136
136
 
137
+ function printableReadVariableNames(term, env, explicit) {
138
+ const names = new Map(explicit);
139
+ const used = new Set(names.values());
140
+ const suffixes = new Map();
141
+ const seenVariables = new Set();
142
+ const seenTerms = new Set();
143
+ const stack = [term];
144
+
145
+ while (stack.length) {
146
+ const current = deref(stack.pop(), env);
147
+ if (current.type === VAR) {
148
+ if (seenVariables.has(current.name)) continue;
149
+ seenVariables.add(current.name);
150
+ if (names.has(current.name) || current.displayName == null) continue;
151
+ const base = writeVariable(current.displayName);
152
+ let candidate = base;
153
+ let suffix = suffixes.get(base) ?? 1;
154
+ while (used.has(candidate)) candidate = `${base}_${suffix++}`;
155
+ suffixes.set(base, suffix);
156
+ names.set(current.name, candidate);
157
+ used.add(candidate);
158
+ continue;
159
+ }
160
+ if (current.type !== COMPOUND || seenTerms.has(current)) continue;
161
+ seenTerms.add(current);
162
+ for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
163
+ }
164
+
165
+ return names;
166
+ }
167
+
137
168
  function format(term, env, options, table, maxPriority = 1200, context = 'term') {
138
169
  const resolved = deref(term, env);
139
- if (resolved.type === VAR) return options.variableNames.get(resolved.name) ?? writeVariable(resolved.name);
170
+ if (resolved.type === VAR) {
171
+ return options.variableNames.get(resolved.name) ?? writeVariable(resolved.displayName ?? resolved.name);
172
+ }
140
173
  if (resolved.type === STRING) return writeString(resolved.name);
141
174
  if (resolved.type === ATOM) {
142
175
  if (!options.quoted) return resolved.name;
@@ -224,12 +257,13 @@ function format(term, env, options, table, maxPriority = 1200, context = 'term')
224
257
  }
225
258
 
226
259
  export function formatTermForWrite(term, env = new Env(), options = {}) {
260
+ const explicitVariableNames = options.variableNames instanceof Map ? options.variableNames : new Map();
227
261
  const normalized = {
228
262
  quoted: options.quoted === true,
229
263
  ignoreOps: options.ignoreOps === true,
230
264
  numbervars: options.numbervars !== false,
231
265
  doubleQuotes: options.doubleQuotes,
232
- variableNames: options.variableNames instanceof Map ? options.variableNames : new Map(),
266
+ variableNames: printableReadVariableNames(term, env, explicitVariableNames),
233
267
  compact: options.compact === true,
234
268
  operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
235
269
  };
@@ -1284,6 +1284,35 @@ c4 ?- call((!;1)).
1284
1284
  assertEqual(result.stderr, '', 'stderr');
1285
1285
  },
1286
1286
  },
1287
+ {
1288
+ name: 'read terms have a variable scope distinct from the calling query',
1289
+ run: () => {
1290
+ const result = runCli([], {
1291
+ input:
1292
+ 'read(X).\n' +
1293
+ 'X=a.\n' +
1294
+ 'read(X).\n' +
1295
+ 'Y=a.\n' +
1296
+ 'read_term(X, [variables(Vs), variable_names(Names), singletons(Singletons)]).\n' +
1297
+ 'X = pair(X, Y).\n' +
1298
+ 'halt.\n',
1299
+ });
1300
+ assertEqual(result.status, 0, 'exit status');
1301
+ assertEqual(result.stdout,
1302
+ '?- |: X = (_A = a).\n' +
1303
+ '?- |: X = (_A = a).\n' +
1304
+ "?- |: X = (_A = pair(_A, _B)), Vs = [_A, _B], Names = ['X' = _A, 'Y' = _B], Singletons = ['Y' = _B].\n" +
1305
+ '?- ',
1306
+ 'stdout');
1307
+ assertEqual(result.stderr, '', 'stderr');
1308
+
1309
+ const distinctReads = run(
1310
+ 'check :- read(A), read(B), A \\== B, write_canonical(pair(A, B)), nl.\n',
1311
+ { goal: 'check', ioOptions: { input: 'V.\nV.\n' } },
1312
+ );
1313
+ assertEqual(distinctReads.stdout, 'pair(V,V_1)\ncheck.\n', 'separate read variable sets');
1314
+ },
1315
+ },
1287
1316
  {
1288
1317
  name: 'REPL term input is on demand in conjunctions and Ctrl-D does not exit the top level',
1289
1318
  run: () => {
@@ -2557,6 +2586,42 @@ open(X) :- candidate(X), \\+ closed(X).
2557
2586
  assertEqual(result.stdout, '300000', 'fresh-variable answer count');
2558
2587
  },
2559
2588
  },
2589
+ {
2590
+ name: 'caught number syntax errors do not exhaust memory on distinct inputs',
2591
+ run: () => {
2592
+ const engineUrl = new URL('../src/index.js', import.meta.url).href;
2593
+ const programText = `
2594
+ :- use_module(library(lists)).
2595
+ alphabet(['0','1','2','3','4','5','6','7','8','9','.']).
2596
+ trial([A,B,C,D,E]) :-
2597
+ alphabet(Chars),
2598
+ member(A, Chars), member(B, Chars), member(C, Chars),
2599
+ member(D, Chars), member(E, Chars),
2600
+ catch(number_chars(_, [A,B,C,D,E]), error(syntax_error(number), _), true).
2601
+ `;
2602
+ const script = `
2603
+ import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
2604
+ const program = Program.parse(${JSON.stringify(programText)});
2605
+ const solver = new Solver(program, { registry: getEyePrologRegistry() });
2606
+ const goal = parseGoalText('trial(Chars)');
2607
+ let count = 0;
2608
+ for (const _ of solver.solve([goal], new Env(), 0)) {
2609
+ if (++count === 150000) break;
2610
+ }
2611
+ if (count !== 150000) throw new Error('unexpected answer count: ' + count);
2612
+ process.stdout.write(String(count));
2613
+ `;
2614
+ const result = spawnSync(process.execPath, [
2615
+ '--max-old-space-size=64',
2616
+ '--input-type=module',
2617
+ '--eval',
2618
+ script,
2619
+ ], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
2620
+ if (result.error) throw result.error;
2621
+ assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
2622
+ assertEqual(result.stdout, '150000', 'distinct number syntax attempts');
2623
+ },
2624
+ },
2560
2625
  {
2561
2626
  name: 'host Map/Set capacity errors become resource_error(memory)',
2562
2627
  run: () => {