eyeprolog 1.1.25 → 1.1.26

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.26",
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) {
@@ -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: () => {
@@ -6300,7 +6300,11 @@ accepted as a negative answer.
6300
6300
  Answer descriptions support ordered answers separated by `;`, acceptable
6301
6301
  alternatives separated by `|`, `true`, `false`, standard error descriptions,
6302
6302
  and the `unexpected` annotation for an answer that must not occur (`inattendue`
6303
- is its synonym). `...` and `ad_infinitum` accept further answers. Multiple
6303
+ is its synonym). Variables named in the query keep their identity inside answer
6304
+ descriptions; variables introduced only by a description are fresh. For example,
6305
+ a query `throw(g(X))` is described by `throw(g(_X))`, while
6306
+ `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
6307
+ variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. Multiple
6304
6308
  indented descriptions after one query must all hold. `inputs/1` supplies and
6305
6309
  checks consumed characters; `outputs/1` checks emitted characters. `sto` marks
6306
6310
  an answer description that this finite-tree implementation skips. `loops` is