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 +1 -1
- package/src/quads.js +27 -7
- package/src/repl.js +13 -1
- package/src/write.js +4 -1
- package/test/run-regression.mjs +35 -0
- package/the-art-of-eyeprolog.md +10 -2
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/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
|
|
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
|
-
|
|
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
|
}
|
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: () => {
|
|
@@ -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: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -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.`.
|
|
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).
|
|
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
|