eyeprolog 1.3.92 → 1.3.93

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.
@@ -250,3 +250,4 @@ npm run generate
250
250
  - [01-color.pl](chapter-40/01-color.pl) — Embedded quad tests
251
251
  - [02-program.pl](chapter-40/02-program.pl)
252
252
  - [03-program-2.pl](chapter-40/03-program-2.pl)
253
+ - [04-program-3.pl](chapter-40/04-program-3.pl)
@@ -1,5 +1,6 @@
1
1
  % From The Art of EyeProlog, Chapter 40.
2
- ?- read(X).
3
- inputs("1."), X = 1, unexpected.
4
- inputs("1."), peeks(" "), X = 1.
5
- inputs("1. "), peeks(" "), X = 1, unexpected.
2
+ ?- dif(X,Y), X = a.
3
+ true, unexpected.
4
+ X = a, unexpected.
5
+ X = a, maybe.
6
+ maybe, unexpected.
@@ -1,5 +1,5 @@
1
1
  % From The Art of EyeProlog, Chapter 40.
2
- inf :- inf, inf.
3
-
4
- ?- inf.
5
- loops.
2
+ ?- read(X).
3
+ inputs("1."), X = 1, unexpected.
4
+ inputs("1."), peeks(" "), X = 1.
5
+ inputs("1. "), peeks(" "), X = 1, unexpected.
@@ -0,0 +1,5 @@
1
+ % From The Art of EyeProlog, Chapter 40.
2
+ inf :- inf, inf.
3
+
4
+ ?- inf.
5
+ loops.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.92",
6
+ "version": "1.3.93",
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
@@ -1,6 +1,8 @@
1
1
  // Embedded quad tests: a query followed by one or more answer descriptions.
2
2
  // The syntax follows the portable "queries using answer descriptions" format
3
3
  // used by Trealla and the ISO Prolog working examples linked from issue #1.
4
+ // A `maybe` annotation denotes a successful answer with residual constraints;
5
+ // it does not relax the ordinary answer-substitution comparison.
4
6
  import {
5
7
  ATOM, COMPOUND, VAR, Env, atom, compound, copyResolved, deref,
6
8
  flattenConjunction, listFromItems, properListItems, termIsGround,
@@ -203,6 +205,7 @@ function describeLeaf(term) {
203
205
  sto: false,
204
206
  false: false,
205
207
  truth: false,
208
+ maybe: false,
206
209
  loops: false,
207
210
  waits: false,
208
211
  error: null,
@@ -218,6 +221,7 @@ function describeLeaf(term) {
218
221
  if (item.name === 'unexpected' || item.name === 'inattendue') leaf.unexpected = true;
219
222
  else if (item.name === '...' || item.name === 'ad_infinitum') leaf.more = true;
220
223
  else if (item.name === 'sto') leaf.sto = true;
224
+ else if (item.name === 'maybe') leaf.maybe = true;
221
225
  else if (item.name === 'loops') leaf.loops = true;
222
226
  else if (item.name === 'waits') leaf.waits = true;
223
227
  else if (item.name === 'other_answer_sequence') {
@@ -253,10 +257,11 @@ function describeLeaf(term) {
253
257
  if (isErrorDescription(item)) leaf.error = item;
254
258
  else leaf.malformed ??= item;
255
259
  }
256
- leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.loops || leaf.waits ||
260
+ leaf.hasExpectation = leaf.bindings.length > 0 || leaf.truth || leaf.false || leaf.maybe || leaf.loops || leaf.waits ||
257
261
  leaf.error != null || leaf.output != null;
258
262
  if (!leaf.hasExpectation && !leaf.more && !leaf.sto && leaf.unsupported == null) leaf.malformed ??= term;
259
263
  if ([leaf.false, leaf.truth, leaf.error != null].filter(Boolean).length > 1) leaf.malformed ??= term;
264
+ if (leaf.maybe && (leaf.false || leaf.loops || leaf.waits || leaf.error != null)) leaf.malformed ??= term;
260
265
  return leaf;
261
266
  }
262
267
 
@@ -386,9 +391,26 @@ function matchLeaf(program, query, leaf, actual, position) {
386
391
  }
387
392
  const solution = actual.solutions[position];
388
393
  if (!solution || !outputMatches(program, leaf.output, solution.output)) return false;
394
+ // Portable answer descriptions distinguish plain success from success with
395
+ // residual constraints. `maybe` requires at least one pending residue, while
396
+ // its absence requires an unconstrained answer. In either case the stated
397
+ // substitutions remain exact and are checked below.
398
+ if (leaf.maybe !== hasPendingConstraints(solution.env)) return false;
389
399
  return substitutionMatches(query, leaf.bindings, solution.env);
390
400
  }
391
401
 
402
+ function hasPendingConstraints(env) {
403
+ for (const constraint of env.variableConstraints?.() ?? []) {
404
+ if (constraint.status?.(env) === 'pending') return true;
405
+ }
406
+ // Prolog attributed variables and delayed goals are the other residual-state
407
+ // mechanisms exposed by call_residue_vars/2. Treat either as pending residue
408
+ // even when a library does not provide a printable projection goal.
409
+ if ((env.attributedVariableNames?.() ?? []).length > 0) return true;
410
+ if ((env.delayedVariableNames?.() ?? []).length > 0) return true;
411
+ return false;
412
+ }
413
+
392
414
  function descriptionDeclaresSto(description) {
393
415
  return splitOperator(description, '|').some((alternative) =>
394
416
  splitOperator(alternative, ';').some((term) => describeLeaf(term).sto));
@@ -2005,6 +2005,25 @@ c4 ?- call((!;1)).
2005
2005
  assertEqual(result.stdout, 'quads: 37 run, 37 passed, 0 failed.\n', 'quad report');
2006
2006
  },
2007
2007
  },
2008
+ {
2009
+ name: 'runQuads maybe requires pending constraints without weakening substitutions (issue #80)',
2010
+ run: () => {
2011
+ const result = publicApi.runQuads(`37
2012
+ ?- dif(X,Y), X = a.
2013
+ true, unexpected.
2014
+ X = a, unexpected.
2015
+ X = a, maybe.
2016
+ maybe, unexpected.
2017
+
2018
+ ?- X = a.
2019
+ X = a, maybe, unexpected.
2020
+ `);
2021
+ assertEqual(result.total, 5, 'quad total');
2022
+ assertEqual(result.passed, 5, 'quad passed');
2023
+ assertEqual(result.failed, 0, 'quad failed');
2024
+ assertEqual(result.stdout, 'quads: 5 run, 5 passed, 0 failed.\n', 'quad report');
2025
+ },
2026
+ },
2008
2027
  {
2009
2028
  name: 'runQuads rejects malformed answer substitutions',
2010
2029
  run: () => {
@@ -3234,22 +3253,18 @@ c4 ?- call((!;1)).
3234
3253
  {
3235
3254
  name: 'REPL releases terminal signals while a query computes',
3236
3255
  run: () => {
3237
- if (process.platform === 'win32') return;
3238
- const available = spawnSync('sh', ['-c',
3239
- 'command -v script >/dev/null 2>&1 && script --version 2>/dev/null | grep -qi util-linux']);
3240
- if (available.status !== 0) return;
3241
- const command = `${shellQuote(process.execPath)} ${shellQuote(bin)}`;
3242
- const scriptCommand =
3243
- `{ printf 'repeat, fail.\n'; sleep 0.2; printf '\\003'; } | ` +
3244
- `script -qefc ${shellQuote(command)} /dev/null`;
3245
- const result = spawnSync('sh', ['-c', scriptCommand], {
3246
- cwd: packageRoot,
3247
- encoding: 'utf8',
3248
- timeout: 3000,
3249
- });
3256
+ if (!hasUtilLinuxScript()) return;
3257
+ // Synchronize on output produced from inside the solver instead of a
3258
+ // fixed sleep: on a loaded host Ctrl-C could otherwise arrive before
3259
+ // the query starts, leaving repeat/fail to run until the test timeout.
3260
+ const result = runScriptedRepl([
3261
+ { waitFor: '?- ', send: 'put_code(82), put_code(69), put_code(65), put_code(68), put_code(89), repeat, fail.\n' },
3262
+ { waitFor: 'READY', send: '\u0003' },
3263
+ ]);
3250
3264
  assertEqual(result.error?.code, undefined, 'terminal interrupt timeout');
3251
3265
  assertEqual(result.status, 130, 'SIGINT exit status');
3252
- assertIncludes(result.stdout, '?- repeat, fail.', 'terminal query echo');
3266
+ assertIncludes(result.stdout, '?- put_code(82), put_code(69), put_code(65), put_code(68), put_code(89), repeat, fail.', 'terminal query echo');
3267
+ assertIncludes(result.stdout, 'READY', 'query entered computation');
3253
3268
  },
3254
3269
  },
3255
3270
  {
@@ -7045,7 +7045,23 @@ is its synonym). Variables named in the query keep their identity inside answer
7045
7045
  descriptions; variables introduced only by a description are fresh. For example,
7046
7046
  a query `throw(g(X))` is described by `throw(g(_X))`, while
7047
7047
  `throw(g(X)), unexpected` verifies that ISO `throw/1` did not retain the query
7048
- variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. Multiple
7048
+ variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. The `maybe` annotation describes
7049
+ a successful answer that still has at least one pending residual constraint. It
7050
+ does not stand for an arbitrary answer and it does not weaken substitution
7051
+ matching: `X = a, maybe` still requires the `X = a` substitution. Conversely, a
7052
+ successful answer description without `maybe` requires that no residual
7053
+ constraint remain. For example, a pending `dif/2` constraint can be checked as:
7054
+
7055
+ ```eyeprolog
7056
+ ?- dif(X,Y), X = a.
7057
+ true, unexpected.
7058
+ X = a, unexpected.
7059
+ X = a, maybe.
7060
+ maybe, unexpected.
7061
+ ```
7062
+
7063
+ EyeProlog treats native variable constraints, attributed-variable residue, and
7064
+ delayed goals as pending residue for this purpose. Multiple
7049
7065
  indented descriptions after one query are independent checks: each re-runs the
7050
7066
  query, each is counted in the `quads:` summary, and a failing description does
7051
7067
  not suppress later descriptions for that query. `inputs/1` supplies and checks