eyeprolog 1.1.25 → 1.1.27

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.1.25",
6
+ "version": "1.1.27",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/quads.js CHANGED
@@ -106,6 +106,11 @@ function checkAlternative(program, quad, alternative, options) {
106
106
  if (leaf.more && !leaf.hasExpectation) return { ok: true };
107
107
  const matches = matchLeaf(quad.query, leaf, actual, position);
108
108
  if (leaf.unexpected ? matches : !matches) return { ok: false };
109
+ // An unexpected error description is a negative assertion about that
110
+ // particular error pattern. A different exception may be present and is
111
+ // checked by other descriptions; do not make the final 'no error' test
112
+ // turn a successful negative match back into a failure.
113
+ if (leaf.unexpected && leaf.error != null) return { ok: true };
109
114
  if (leaf.more) return { ok: true };
110
115
  if (!leaf.unexpected && (leaf.false || leaf.error != null)) {
111
116
  return { ok: position === leaves.length - 1 };
@@ -252,7 +257,7 @@ function matchLeaf(query, leaf, actual, position) {
252
257
  }
253
258
  if (leaf.error != null) {
254
259
  return position === actual.solutions.length && actual.error != null &&
255
- errorMatches(leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
260
+ errorMatches(query, leaf.error, actual.error.term) && outputMatches(leaf.output, actual.error.output);
256
261
  }
257
262
  const solution = actual.solutions[position];
258
263
  if (!solution || !outputMatches(leaf.output, solution.output)) return false;
@@ -297,12 +302,15 @@ function namedVariables(term) {
297
302
  return found;
298
303
  }
299
304
 
300
- function patternVariant(pattern, patternEnv, actual, actualEnv, pairs = new Map(), reverse = new Map()) {
305
+ function patternVariant(
306
+ pattern, patternEnv, actual, actualEnv, pairs = new Map(), reverse = new Map(), fixedPatternNames = null,
307
+ ) {
301
308
  pattern = deref(pattern, patternEnv);
302
309
  actual = deref(actual, actualEnv);
303
310
  if (pattern.type === ATOM && pattern.name === '...') return true;
304
311
  if (pattern.type === VAR || actual.type === VAR) {
305
312
  if (pattern.type !== VAR || actual.type !== VAR) return false;
313
+ if (fixedPatternNames?.has(pattern.name)) return pattern.name === actual.name;
306
314
  const paired = pairs.get(pattern.name);
307
315
  const reversed = reverse.get(actual.name);
308
316
  if (paired != null || reversed != null) return paired === actual.name && reversed === pattern.name;
@@ -312,7 +320,9 @@ function patternVariant(pattern, patternEnv, actual, actualEnv, pairs = new Map(
312
320
  }
313
321
  if (pattern.type !== actual.type || pattern.name !== actual.name || pattern.arity !== actual.arity) return false;
314
322
  for (let index = 0; index < pattern.arity; index++) {
315
- if (!patternVariant(pattern.args[index], patternEnv, actual.args[index], actualEnv, pairs, reverse)) return false;
323
+ if (!patternVariant(
324
+ pattern.args[index], patternEnv, actual.args[index], actualEnv, pairs, reverse, fixedPatternNames,
325
+ )) return false;
316
326
  }
317
327
  return true;
318
328
  }
@@ -334,16 +344,26 @@ function errorTerm(error) {
334
344
  return compound('error', [atom('system_error'), variable('$quad_context')]);
335
345
  }
336
346
 
337
- function errorMatches(expected, actual) {
347
+ function errorMatches(query, expected, actual) {
348
+ // Variables named in the query keep their identity in answer descriptions.
349
+ // Seed both directions so a query variable can match only that same query
350
+ // variable, while variables introduced by the description (for example _X)
351
+ // remain fresh pattern variables. This is especially important for throw/1:
352
+ // ISO requires the thrown ball to be a renamed copy, so throw(g(_X)) may
353
+ // describe throw(g(X)), but throw(g(X)) must not.
354
+ const queryNames = new Set(namedVariables(query).map((item) => item.name));
355
+ const matches = (pattern, term) => patternVariant(
356
+ pattern, new Env(), term, new Env(), new Map(), new Map(), queryNames);
357
+
338
358
  if (expected.type === COMPOUND && expected.name === 'throw' && expected.arity === 1 &&
339
359
  actual.type === COMPOUND && actual.name === '$quad_thrown' && actual.arity === 1) {
340
- return patternVariant(expected.args[0], new Env(), actual.args[0], new Env());
360
+ return matches(expected.args[0], actual.args[0]);
341
361
  }
342
362
  if (actual.type !== COMPOUND || actual.name !== 'error' || actual.arity !== 2) return false;
343
363
  if (expected.type === COMPOUND && expected.name === 'error' && expected.arity === 2) {
344
- return patternVariant(expected, new Env(), actual, new Env());
364
+ return matches(expected, actual);
345
365
  }
346
- return patternVariant(expected, new Env(), actual.args[0], new Env());
366
+ return matches(expected, actual.args[0]);
347
367
  }
348
368
 
349
369
  function isErrorDescription(term) {
package/src/repl.js CHANGED
@@ -370,6 +370,17 @@ function formatAnswer(engine, state, variables, env) {
370
370
  const bindings = [];
371
371
  const queryVariableNames = new Set(variables.map((variable) => variable.name));
372
372
  const names = new Map(variables.map((variable) => [variable.name, variable.name]));
373
+ const operators = [...state.program.operators.values()];
374
+ // Answer substitutions are displayed as `Variable = Value`. Format Value
375
+ // in the right-operand context of the active infix `=/2` definition so an
376
+ // operator term such as `a = b` is parenthesized rather than producing the
377
+ // invalid `T = a = b`. ISO does not standardize a top level, but the term
378
+ // syntax used by the display must still respect the current operator table.
379
+ const equality = operators.find((definition) =>
380
+ definition.name === '=' && ['xfx', 'xfy', 'yfx'].includes(definition.specifier));
381
+ const valueMaxPriority = equality == null
382
+ ? 699
383
+ : equality.specifier === 'xfy' ? equality.priority : equality.priority - 1;
373
384
  let generated = 0;
374
385
 
375
386
  for (const variable of variables) collectUnboundVariables(engine, variable, env, names, () => `_${letterName(generated++)}`);
@@ -379,8 +390,9 @@ function formatAnswer(engine, state, variables, env) {
379
390
  (value.name === variable.name || !queryVariableNames.has(value.name))) continue;
380
391
  bindings.push(`${variable.name} = ${engine.formatTermForWrite(value, env, {
381
392
  quoted: true,
382
- operators: [...state.program.operators.values()],
393
+ operators,
383
394
  variableNames: names,
395
+ maxPriority: valueMaxPriority,
384
396
  })}`);
385
397
  }
386
398
  return bindings.length === 0 ? 'true' : bindings.join(', ');
package/src/write.js CHANGED
@@ -208,5 +208,8 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
208
208
  compact: options.compact === true,
209
209
  operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
210
210
  };
211
- return format(term, env, normalized, operatorTable(options.operators), 1200);
211
+ const maxPriority = Number.isInteger(options.maxPriority)
212
+ ? Math.max(0, Math.min(1200, options.maxPriority))
213
+ : 1200;
214
+ return format(term, env, normalized, operatorTable(options.operators), maxPriority);
212
215
  }
@@ -243,6 +243,24 @@ why(
243
243
  assertEqual(result.stdout, 'quads: 12 run, 12 passed, 0 failed.\n', 'quad report');
244
244
  },
245
245
  },
246
+ {
247
+ name: 'runQuads distinguishes query variables from renamed throw variables',
248
+ run: () => {
249
+ const source = `?- throw(g(X)).\n` +
250
+ ` throw(g(_X)).\n` +
251
+ ` throw(g(X)), unexpected.\n`;
252
+ const result = publicApi.runQuads(Program.parseSources([{ text: source, filename: 'throw-copy-quad.pl' }]));
253
+ assertEqual(result.total, 1, 'quad total');
254
+ assertEqual(result.passed, 1, 'quad passed');
255
+ assertEqual(result.failed, 0, 'quad failed');
256
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
257
+
258
+ const forbiddenFresh = publicApi.runQuads(
259
+ `?- throw(g(X)).\n throw(g(_X)), unexpected.\n`,
260
+ );
261
+ assertEqual(forbiddenFresh.failed, 1, 'fresh thrown variable is detected');
262
+ },
263
+ },
246
264
  {
247
265
  name: 'runQuads matches the corrected ISO phrase quad boundaries',
248
266
  run: () => {
@@ -567,6 +585,23 @@ c4 ?- call((!;1)).
567
585
  assertEqual(result.stderr, '', 'stderr');
568
586
  },
569
587
  },
588
+ {
589
+ name: 'REPL parenthesizes operator-valued answer substitutions',
590
+ run: () => {
591
+ const result = runCli([], {
592
+ input: 'T = (a=b).\nU = (a,b).\nV = (a;b).\nW = (a+b).\nhalt.\n',
593
+ });
594
+ assertEqual(result.status, 0, 'exit status');
595
+ assertEqual(result.stdout,
596
+ '?- T = (a = b).\n' +
597
+ '?- U = (a, b).\n' +
598
+ '?- V = (a ; b).\n' +
599
+ '?- W = a + b.\n' +
600
+ '?- ',
601
+ 'stdout');
602
+ assertEqual(result.stderr, '', 'stderr');
603
+ },
604
+ },
570
605
  {
571
606
  name: 'REPL accepts multiline period-terminated queries',
572
607
  run: () => {
@@ -6191,7 +6191,11 @@ When another answer exists in an interactive terminal, press `;`, Space, or
6191
6191
  enumeration, `a` enumerates all remaining answers, `f` enumerates the next
6192
6192
  five, and `h` displays the answer-control help. A
6193
6193
  period-terminated query with no solutions prints `false.`; a solution without
6194
- visible variable bindings prints `true.`. Use `[file].` or `['file.pl'].` to
6194
+ visible variable bindings prints `true.`. Answer substitutions are rendered as
6195
+ valid Prolog syntax under the current operator table: when a bound value would
6196
+ not be a valid right operand of the displayed `=/2`, EyeProlog adds parentheses,
6197
+ for example `T = (a = b).` rather than the invalid `T = a = b.`. Use `[file].`
6198
+ or `['file.pl'].` to
6195
6199
  consult local source, and `halt.` or `halt(Status).` to leave the top level.
6196
6200
  Up and Down recall queries from the current session. Explicit `eyeprolog -h`
6197
6201
  displays command-line help.
@@ -6300,7 +6304,11 @@ accepted as a negative answer.
6300
6304
  Answer descriptions support ordered answers separated by `;`, acceptable
6301
6305
  alternatives separated by `|`, `true`, `false`, standard error descriptions,
6302
6306
  and the `unexpected` annotation for an answer that must not occur (`inattendue`
6303
- is its synonym). `...` and `ad_infinitum` accept further answers. Multiple
6307
+ is its synonym). Variables named in the query keep their identity inside answer
6308
+ descriptions; variables introduced only by a description are fresh. For example,
6309
+ a query `throw(g(X))` is described by `throw(g(_X))`, while
6310
+ `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
6311
+ variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. Multiple
6304
6312
  indented descriptions after one query must all hold. `inputs/1` supplies and
6305
6313
  checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
6306
6314
  an answer description that this finite-tree implementation skips. `loops` is