eyeprolog 1.3.87 → 1.3.88

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.3.87",
6
+ "version": "1.3.88",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -36,7 +36,9 @@ Annotations are persistent across `Env.clone()` and therefore backtrack with the
36
36
  substitution. Language services may attach immutable constraint descriptors,
37
37
  but the unconstrained unification hot path only performs a null check; descriptor
38
38
  validation and reindexing run only in environments that actually contain
39
- annotations. `dif/2` is the first descriptor-based user of this mechanism.
39
+ annotations. Descriptors can optionally define logical subsumption so the
40
+ store retains only its strongest pending constraints. `dif/2` is the first
41
+ descriptor-based user of this mechanism.
40
42
 
41
43
  `atts.js` layers Prolog-visible attributed variables over that same persistent
42
44
  environment. Per-module attributes follow the current variable representative.
package/src/iso.js CHANGED
@@ -211,6 +211,19 @@ function difConstraint(left, right) {
211
211
  skipAttributeHooks: true,
212
212
  }) ? 'pending' : 'entailed';
213
213
  },
214
+ subsumes(other, env) {
215
+ if (other?.kind !== 'dif') return false;
216
+ // dif(A,B) implies dif(C,D) exactly when C=D entails A=B. Apply the
217
+ // most general finite-tree unifier for the other's forbidden equality;
218
+ // if this equality makes our terms identical, every instance forbidden
219
+ // by the other constraint is already forbidden by this one.
220
+ const probe = env.clone();
221
+ if (!unify(other.left, other.right, probe, {
222
+ skipVariableConstraints: true,
223
+ skipAttributeHooks: true,
224
+ })) return true;
225
+ return identical(left, right, probe);
226
+ },
214
227
  residualGoal() {
215
228
  return compound('dif', [left, right]);
216
229
  },
package/src/term.js CHANGED
@@ -465,9 +465,24 @@ export class Env {
465
465
  typeof constraint.status !== 'function') {
466
466
  throw new TypeError('variable constraint requires variables(env) and status(env)');
467
467
  }
468
- const constraints = new Set(this._variableConstraints ?? []);
469
- constraints.add(constraint);
470
- this._variableConstraints = constraints;
468
+ this._setNormalizedVariableConstraints([...(this._variableConstraints ?? []), constraint]);
469
+ }
470
+ _setNormalizedVariableConstraints(constraints) {
471
+ // Descriptors may define logical subsumption. Keep the strongest pending
472
+ // constraints so equivalent, symmetric, or weaker residual goals do not
473
+ // accumulate, while unrelated descriptor kinds remain untouched.
474
+ const normalized = [];
475
+ candidateLoop:
476
+ for (const candidate of constraints) {
477
+ for (const existing of normalized) {
478
+ if (existing === candidate || existing.subsumes?.(candidate, this) === true) continue candidateLoop;
479
+ }
480
+ for (let index = normalized.length - 1; index >= 0; index--) {
481
+ if (candidate.subsumes?.(normalized[index], this) === true) normalized.splice(index, 1);
482
+ }
483
+ normalized.push(candidate);
484
+ }
485
+ this._variableConstraints = normalized.length === 0 ? null : new Set(normalized);
471
486
  this._reindexVariableConstraints();
472
487
  }
473
488
  variableConstraints(kind = null) {
@@ -487,8 +502,7 @@ export class Env {
487
502
  if (status === 'violated') return false;
488
503
  if (status !== 'entailed') pending.add(constraint);
489
504
  }
490
- this._variableConstraints = pending.size === 0 ? null : pending;
491
- this._reindexVariableConstraints();
505
+ this._setNormalizedVariableConstraints(pending);
492
506
  return true;
493
507
  }
494
508
  _reindexVariableConstraints() {
@@ -210,6 +210,55 @@ function regressionCases() {
210
210
  'freeze/2 residual projection terminates alongside dif/2 (issue #73)');
211
211
  },
212
212
  },
213
+ {
214
+ name: 'dif/2 removes symmetric and logically weaker residual constraints (issue #79)',
215
+ run: () => {
216
+ const residuals = runCli([], {
217
+ input:
218
+ 'dif(X,Y),dif(Y,X).\n' +
219
+ 'dif(X,Y),dif(X-Y,Z-Z).\n' +
220
+ 'dif(X-Y,Z-Z),dif(X,Y).\n' +
221
+ 'dif(X,a),dif(X,b).\n' +
222
+ 'dif(X,a),dif(Y,a),X=Y.\n' +
223
+ 'halt.\n',
224
+ });
225
+ assertEqual(residuals.status, 0, 'dif/2 normalization display status');
226
+ assertEqual(residuals.stdout,
227
+ '?- dif(X, Y).\n' +
228
+ '?- dif(X, Y).\n' +
229
+ '?- dif(X, Y).\n' +
230
+ '?- dif(X, a), dif(X, b).\n' +
231
+ '?- X = Y, dif(Y, a).\n' +
232
+ '?- ',
233
+ 'dif/2 residuals are minimal independent of insertion order');
234
+ assertEqual(residuals.stderr, '', 'dif/2 normalization display stderr');
235
+
236
+ const pendingDifCount = (text) => {
237
+ const solver = new Solver(Program.parse(''), { registry: getEyePrologRegistry() });
238
+ const solutions = solver.solve([parseGoalText(text)], new Env(), 0);
239
+ const answer = solutions.next();
240
+ assertEqual(answer.done, false, `${text} succeeds`);
241
+ const count = answer.value.variableConstraints('dif').length;
242
+ solutions.return();
243
+ return count;
244
+ };
245
+ assertEqual(pendingDifCount('dif(X,Y),dif(Y,X)'), 1,
246
+ 'symmetric disequalities share one descriptor');
247
+ assertEqual(pendingDifCount('dif(X-Y,Z-Z),dif(X,Y)'), 1,
248
+ 'stronger later disequality replaces the weaker descriptor');
249
+ assertEqual(pendingDifCount('dif(X,a),dif(X,b)'), 2,
250
+ 'independent disequalities are both retained');
251
+ assertEqual(pendingDifCount('dif(X,a),dif(Y,a),X=Y'), 1,
252
+ 'later aliasing coalesces newly equivalent disequalities');
253
+ for (const prefix of [
254
+ 'dif(X,Y),dif(X-Y,Z-Z)',
255
+ 'dif(X-Y,Z-Z),dif(X,Y)',
256
+ ]) {
257
+ assertEqual(run('', { goal: `${prefix},X=Y` }).stats.completed_goal_lists, 0,
258
+ `${prefix} retains the stronger disequality semantics`);
259
+ }
260
+ },
261
+ },
213
262
  {
214
263
  name: 'library(atts) provides Scryer-style attributed-variable hooks',
215
264
  run: () => {
@@ -6284,6 +6284,12 @@ otherwise it delays the goal until `Term` becomes nonvariable. Suspensions are
6284
6284
  kept in the logical environment, so bindings and backtracking remain isolated
6285
6285
  between solution branches.
6286
6286
 
6287
+ `dif(?Left,?Right)` posts a delayed finite-tree disequality when its arguments
6288
+ can still unify. Its residual store is normalized by logical implication:
6289
+ symmetric or equivalent constraints share one residual, and a stronger
6290
+ constraint removes weaker ones regardless of insertion order. Independent
6291
+ disequalities remain separate.
6292
+
6287
6293
  <!-- eyeprolog-library-catalog:start -->
6288
6294
 
6289
6295
  | Module | Exported predicate indicators | Primary role |