eyeprolog 1.3.92 → 1.3.94

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.94",
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));
package/src/solver.js CHANGED
@@ -524,14 +524,21 @@ export class Solver {
524
524
  if (this.solutionsSeen >= this.solutionLimit) break;
525
525
 
526
526
  const pendingAttributeGoals = env.takePendingAttributeGoals?.() ?? [];
527
- if (pendingAttributeGoals.length > 0) goals = [...pendingAttributeGoals, ...goals];
527
+ if (pendingAttributeGoals.length > 0) {
528
+ // Goals produced by verify_attributes/3 are invoked by the attribute
529
+ // mechanism, not textually spliced into the caller. Give every
530
+ // awakened goal the same opaque cut boundary as call/1 so a delayed
531
+ // cut cannot prune choices that were made before the suspension.
532
+ const awakened = pendingAttributeGoals.map((pending) => compound('call', [pending]));
533
+ goals = [...awakened, ...goals];
534
+ }
528
535
 
529
536
  const readyDelays = env.takeReadyDelays();
530
537
  if (readyDelays.length > 0) {
531
538
  const awakened = readyDelays.map(({ goal, module }) => {
532
539
  const delayed = copyResolved(goal, env);
533
540
  qualifyTerm(delayed, module);
534
- return delayed;
541
+ return compound('call', [delayed]);
535
542
  });
536
543
  goals = [...awakened, ...goals];
537
544
  }
@@ -1990,6 +1990,22 @@ c4 ?- call((!;1)).
1990
1990
  assertEqual(result.stdout, 'wake(ready, awake).\n', 'freeze answer');
1991
1991
  },
1992
1992
  },
1993
+ {
1994
+ name: 'Prologue freeze keeps awakened cuts inside the delayed call (issue #81)',
1995
+ run: () => {
1996
+ const result = publicApi.runQuads(`:- use_module(library(prologue)).
1997
+ 38
1998
+ ?- call( ( (Y=1;Y=2), freeze(X,!), X = c ) ); Y = 3.
1999
+ Y = 1, X = c
2000
+ ; Y = 2, X = c
2001
+ ; Y = 3.
2002
+ `);
2003
+ assertEqual(result.total, 1, 'quad total');
2004
+ assertEqual(result.passed, 1, 'quad passed');
2005
+ assertEqual(result.failed, 0, 'quad failed');
2006
+ assertEqual(result.stdout, 'quads: 1 run, 1 passed, 0 failed.\n', 'quad report');
2007
+ },
2008
+ },
1993
2009
  {
1994
2010
  name: 'runQuads covers the remaining finite Prologue examples and arities',
1995
2011
  run: () => {
@@ -2005,6 +2021,25 @@ c4 ?- call((!;1)).
2005
2021
  assertEqual(result.stdout, 'quads: 37 run, 37 passed, 0 failed.\n', 'quad report');
2006
2022
  },
2007
2023
  },
2024
+ {
2025
+ name: 'runQuads maybe requires pending constraints without weakening substitutions (issue #80)',
2026
+ run: () => {
2027
+ const result = publicApi.runQuads(`37
2028
+ ?- dif(X,Y), X = a.
2029
+ true, unexpected.
2030
+ X = a, unexpected.
2031
+ X = a, maybe.
2032
+ maybe, unexpected.
2033
+
2034
+ ?- X = a.
2035
+ X = a, maybe, unexpected.
2036
+ `);
2037
+ assertEqual(result.total, 5, 'quad total');
2038
+ assertEqual(result.passed, 5, 'quad passed');
2039
+ assertEqual(result.failed, 0, 'quad failed');
2040
+ assertEqual(result.stdout, 'quads: 5 run, 5 passed, 0 failed.\n', 'quad report');
2041
+ },
2042
+ },
2008
2043
  {
2009
2044
  name: 'runQuads rejects malformed answer substitutions',
2010
2045
  run: () => {
@@ -3234,22 +3269,18 @@ c4 ?- call((!;1)).
3234
3269
  {
3235
3270
  name: 'REPL releases terminal signals while a query computes',
3236
3271
  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
- });
3272
+ if (!hasUtilLinuxScript()) return;
3273
+ // Synchronize on output produced from inside the solver instead of a
3274
+ // fixed sleep: on a loaded host Ctrl-C could otherwise arrive before
3275
+ // the query starts, leaving repeat/fail to run until the test timeout.
3276
+ const result = runScriptedRepl([
3277
+ { waitFor: '?- ', send: 'put_code(82), put_code(69), put_code(65), put_code(68), put_code(89), repeat, fail.\n' },
3278
+ { waitFor: 'READY', send: '\u0003' },
3279
+ ]);
3250
3280
  assertEqual(result.error?.code, undefined, 'terminal interrupt timeout');
3251
3281
  assertEqual(result.status, 130, 'SIGINT exit status');
3252
- assertIncludes(result.stdout, '?- repeat, fail.', 'terminal query echo');
3282
+ assertIncludes(result.stdout, '?- put_code(82), put_code(69), put_code(65), put_code(68), put_code(89), repeat, fail.', 'terminal query echo');
3283
+ assertIncludes(result.stdout, 'READY', 'query entered computation');
3253
3284
  },
3254
3285
  },
3255
3286
  {
@@ -6283,7 +6283,11 @@ private helpers and same-named predicates in different modules separate.
6283
6283
  `freeze(?Term,:Goal)` runs `Goal` immediately when `Term` is already nonvariable;
6284
6284
  otherwise it delays the goal until `Term` becomes nonvariable. Suspensions are
6285
6285
  kept in the logical environment, so bindings and backtracking remain isolated
6286
- between solution branches.
6286
+ between solution branches. When a suspension wakes, its goal is meta-invoked
6287
+ with its own cut scope: a `!` inside the delayed goal may commit choices made by
6288
+ that invocation, but it cannot prune alternatives that were created before the
6289
+ `freeze/2` call. Thus `call(((Y=1;Y=2),freeze(X,!),X=c));Y=3` retains the three
6290
+ answers `Y=1`, `Y=2`, and `Y=3`.
6287
6291
 
6288
6292
  `dif(?Left,?Right)` posts a delayed finite-tree disequality when its arguments
6289
6293
  can still unify. Its residual store is normalized by logical implication:
@@ -7045,7 +7049,23 @@ is its synonym). Variables named in the query keep their identity inside answer
7045
7049
  descriptions; variables introduced only by a description are fresh. For example,
7046
7050
  a query `throw(g(X))` is described by `throw(g(_X))`, while
7047
7051
  `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
7052
+ variable in the renamed exception term. `...` and `ad_infinitum` accept further answers. The `maybe` annotation describes
7053
+ a successful answer that still has at least one pending residual constraint. It
7054
+ does not stand for an arbitrary answer and it does not weaken substitution
7055
+ matching: `X = a, maybe` still requires the `X = a` substitution. Conversely, a
7056
+ successful answer description without `maybe` requires that no residual
7057
+ constraint remain. For example, a pending `dif/2` constraint can be checked as:
7058
+
7059
+ ```eyeprolog
7060
+ ?- dif(X,Y), X = a.
7061
+ true, unexpected.
7062
+ X = a, unexpected.
7063
+ X = a, maybe.
7064
+ maybe, unexpected.
7065
+ ```
7066
+
7067
+ EyeProlog treats native variable constraints, attributed-variable residue, and
7068
+ delayed goals as pending residue for this purpose. Multiple
7049
7069
  indented descriptions after one query are independent checks: each re-runs the
7050
7070
  query, each is counted in the `quads:` summary, and a failing description does
7051
7071
  not suppress later descriptions for that query. `inputs/1` supplies and checks