eyeprolog 1.3.74 → 1.3.76

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.74",
6
+ "version": "1.3.76",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -202,7 +202,12 @@ function difConstraint(left, right) {
202
202
  status(env) {
203
203
  if (identical(left, right, env)) return 'violated';
204
204
  const probe = env.clone();
205
- return unify(left, right, probe, { skipVariableConstraints: true }) ? 'pending' : 'entailed';
205
+ // Test structural unifiability without waking freeze/2 or other
206
+ // attributed-variable hooks in this disposable environment.
207
+ return unify(left, right, probe, {
208
+ skipVariableConstraints: true,
209
+ skipAttributeHooks: true,
210
+ }) ? 'pending' : 'entailed';
206
211
  },
207
212
  residualGoal() {
208
213
  return compound('dif', [left, right]);
@@ -218,8 +223,12 @@ function* difBuiltin({ goal, env }) {
218
223
  // In EyeProlog's finite-tree model, terms that cannot unify are already
219
224
  // provably different and need no residual constraint. Otherwise attach a
220
225
  // disequality constraint to every currently unbound variable it mentions.
226
+ // This probe must not wake goals attached to those variables.
221
227
  const probe = env.clone();
222
- if (!unify(left, right, probe, { skipVariableConstraints: true })) {
228
+ if (!unify(left, right, probe, {
229
+ skipVariableConstraints: true,
230
+ skipAttributeHooks: true,
231
+ })) {
223
232
  yield env;
224
233
  return;
225
234
  }
@@ -2556,19 +2565,32 @@ function residueVariablesSince(snapshot, env) {
2556
2565
  return names;
2557
2566
  }
2558
2567
 
2559
- function* callResidueVarsBuiltin({ solver, goal, env }) {
2568
+ function callResidueVarsBuiltin({ solver, goal, env }) {
2560
2569
  const invoked = callable(goal.args[0], env);
2561
2570
  if (invoked.module == null) invoked.module = goal.module ?? 'user';
2562
2571
  const snapshot = residueSnapshot(env);
2563
2572
  const child = solver.cloneForInnerGoal();
2564
- try {
2565
- for (const answerEnv of child.solve([invoked], env, 0)) {
2566
- const next = answerEnv.clone();
2567
- if (unify(goal.args[1], listFromItems(residueVariablesSince(snapshot, answerEnv)), next)) yield next;
2573
+ let pending = true;
2574
+ // The wrapper iterator is resumable even for a deterministic Goal. Expose
2575
+ // the child's actual search state so the top level does not print a phantom
2576
+ // choicepoint after the final answer.
2577
+ const iterator = (function* residueSolutions() {
2578
+ try {
2579
+ for (const answerEnv of child.solve([invoked], env, 0)) {
2580
+ pending = child.hasPendingAlternatives();
2581
+ const next = answerEnv.clone();
2582
+ if (unify(goal.args[1], listFromItems(residueVariablesSince(snapshot, answerEnv)), next)) {
2583
+ yield next;
2584
+ if (!pending) return;
2585
+ }
2586
+ }
2587
+ pending = false;
2588
+ } finally {
2589
+ solver.absorbStatsFrom(child);
2568
2590
  }
2569
- } finally {
2570
- solver.absorbStatsFrom(child);
2571
- }
2591
+ })();
2592
+ iterator.hasPendingAlternatives = () => pending;
2593
+ return iterator;
2572
2594
  }
2573
2595
 
2574
2596
  function* freezeBuiltin({ solver, goal, env }) {
package/src/repl.js CHANGED
@@ -722,6 +722,12 @@ function formatAnswer(engine, state, variables, env) {
722
722
  : equality.specifier === 'xfy' ? equality.priority : equality.priority - 1;
723
723
  let generated = 0;
724
724
 
725
+ // Meta-predicate wrappers can make a query variable alias an internal fresh
726
+ // variable. Keep residual constraints tied to the query's visible name.
727
+ for (const variable of variables) {
728
+ const root = engine.deref(variable, env);
729
+ if (root.type === 'var' && !names.has(root.name)) names.set(root.name, variable.name);
730
+ }
725
731
  for (const variable of variables) collectUnboundVariables(engine, variable, env, names, () => `_${letterName(generated++)}`);
726
732
  for (const variable of variables) {
727
733
  const value = engine.deref(variable, env);
package/src/term.js CHANGED
@@ -650,6 +650,7 @@ export function unify(left, right, env, options = {}) {
650
650
  // unification: a variable cannot be bound to a term containing itself.
651
651
  // Bindings are written into the supplied Env.
652
652
  const occursCheckHandler = options.occursCheck === 'fail' ? null : env?._occursCheckHandler;
653
+ const runAttributeHooks = options.skipAttributeHooks !== true;
653
654
  // Callers may provide a proof that selected variables cannot occur in the
654
655
  // term they are about to receive. Source-level first-use analysis and a few
655
656
  // construction fast paths share this internal proof; ordinary unification
@@ -663,7 +664,7 @@ export function unify(left, right, env, options = {}) {
663
664
 
664
665
  if (a.type === VAR && b.type === VAR && a.name === b.name) continue;
665
666
  if (a.type === VAR && b.type === VAR) {
666
- if (env?._prologAttributes != null) {
667
+ if (runAttributeHooks && env?._prologAttributes != null) {
667
668
  if (!env.preparePrologAttributeUnification(a, b)) return false;
668
669
  a = deref(a, env);
669
670
  b = deref(b, env);
@@ -694,7 +695,7 @@ export function unify(left, right, env, options = {}) {
694
695
  occursCheckHandler?.(a, b, env);
695
696
  return false;
696
697
  }
697
- if (env?._prologAttributes != null && env.hasPrologAttributes(a.name)) {
698
+ if (runAttributeHooks && env?._prologAttributes != null && env.hasPrologAttributes(a.name)) {
698
699
  if (!env.preparePrologAttributeUnification(a, b)) return false;
699
700
  a = deref(a, env);
700
701
  b = deref(b, env);
@@ -712,7 +713,7 @@ export function unify(left, right, env, options = {}) {
712
713
  occursCheckHandler?.(b, a, env);
713
714
  return false;
714
715
  }
715
- if (env?._prologAttributes != null && env.hasPrologAttributes(b.name)) {
716
+ if (runAttributeHooks && env?._prologAttributes != null && env.hasPrologAttributes(b.name)) {
716
717
  if (!env.preparePrologAttributeUnification(b, a)) return false;
717
718
  b = deref(b, env);
718
719
  a = deref(a, env);
@@ -127,6 +127,17 @@ function regressionCases() {
127
127
  'scalar-fact fast matching still validates annotated variables');
128
128
  assertEqual(run('p(b).', { goal: 'dif(X,a),p(X)' }).stats.completed_goal_lists, 1,
129
129
  'scalar-fact fast matching can discharge an annotated disequality');
130
+
131
+ const freezeProgram = Program.parse(':- use_module(library(freeze), [freeze/2]).');
132
+ const freezeSolver = new Solver(freezeProgram);
133
+ const freezeDifGoal = parseGoalText('freeze(X,a),dif(X,c)');
134
+ const freezeDifAnswer = freezeSolver.solve([freezeDifGoal], new Env(), 0).next();
135
+ assertEqual(freezeDifAnswer.done, false, 'freeze/2 and dif/2 answer');
136
+ const freezeResiduals = freezeSolver.attributeResidualGoals(
137
+ freezeDifGoal.args[0].args[0], freezeDifAnswer.value,
138
+ );
139
+ assertEqual(freezeResiduals.length, 1,
140
+ 'freeze/2 residual projection terminates alongside dif/2 (issue #73)');
130
141
  },
131
142
  },
132
143
  {
@@ -178,8 +189,19 @@ check :- call_residue_vars(X in 1..3, Vs), Vs = [X].
178
189
  `;
179
190
  assertIncludes(run(clpzResidue, { goal: 'check' }).stdout, 'check.', 'call_residue_vars/2 includes CLP(Z) attributed variables');
180
191
 
192
+ const deterministicProgram = Program.parse(':- use_module(library(atts)).');
193
+ const deterministicSolver = new Solver(deterministicProgram);
194
+ const deterministicGoal = parseGoalText('call_residue_vars(true, Xs)');
195
+ const deterministicSolutions = deterministicSolver.solve([deterministicGoal], new Env(), 0);
196
+ const deterministicAnswer = deterministicSolutions.next();
197
+ assertEqual(deterministicAnswer.done, false, 'call_residue_vars/2 deterministic answer');
198
+ assertEqual(deterministicSolver.hasPendingAlternatives(), false,
199
+ 'call_residue_vars/2 deterministic success has no leftover choicepoint');
200
+ deterministicSolutions.return();
201
+
181
202
  const residualFile = path.join(tmp, `atts-residual-${++tmpCounter}.pl`);
182
203
  fs.writeFileSync(residualFile, `:- use_module(library(atts)).
204
+ :- use_module(library(freeze), [freeze/2]).
183
205
  :- attribute required/1.
184
206
  attach(X,V) :- put_atts(X, required(V)).
185
207
  verify_attributes(Var, Other, Goals) :-
@@ -189,9 +211,13 @@ verify_attributes(Var, Other, Goals) :-
189
211
  same(X,X).
190
212
  attribute_goals(X) --> { get_atts(X, required(V)) }, [required(X,V)].
191
213
  `);
192
- const repl = runCli([], { input: `[${sourceAtom(residualFile)}].\nattach(X,a).\nhalt.\n` });
214
+ const repl = runCli([], {
215
+ input: `[${sourceAtom(residualFile)}].\nattach(X,a).\nfreeze(Y,a),dif(Y,c).\nhalt.\n`,
216
+ });
193
217
  assertEqual(repl.status, 0, 'attribute residual REPL status');
194
218
  assertIncludes(repl.stdout, 'required(X, a).', 'attribute_goals//1 residual projection');
219
+ assertIncludes(repl.stdout, 'dif(Y, c), freeze:freeze(Y, a).',
220
+ 'freeze/2 and dif/2 residuals share the query variable (issue #73)');
195
221
 
196
222
  const scryerShape = Program.parse(`:- module(scryer_shape, [op(700, xfx, #=), probe/0]).
197
223
  :- op(700, xfx, #=).