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 +1 -1
- package/src/quads.js +27 -7
- package/test/run-regression.mjs +18 -0
- package/the-art-of-eyeprolog.md +5 -1
package/package.json
CHANGED
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(
|
|
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(
|
|
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
|
|
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
|
|
364
|
+
return matches(expected, actual);
|
|
345
365
|
}
|
|
346
|
-
return
|
|
366
|
+
return matches(expected, actual.args[0]);
|
|
347
367
|
}
|
|
348
368
|
|
|
349
369
|
function isErrorDescription(term) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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).
|
|
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
|